Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/load completion improvements #924

Merged
merged 16 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 65 additions & 47 deletions bash_completion
Original file line number Diff line number Diff line change
Expand Up @@ -2496,9 +2496,24 @@ complete -F _minimal ''

__load_completion()
{
local cmd="${1##*/}" dir compfile
local cmd=$1 cmdname=${1##*/} dir compfile
local -a paths
[[ $cmd ]] || return 1
[[ $cmdname ]] || return 1

local backslash=
if [[ $cmd == \\* ]]; then
cmd=${cmd:1}
# If we already have a completion for the "real" command, use it
$(complete -p "$cmd" 2>/dev/null || echo false) "\\$cmd" && return 0
backslash=\\
fi

# Try to resolve real path to $cmd, and make it absolute
local ret realcmd="" origcmd=$cmd
if _comp_realcommand "$cmd"; then
realcmd=$ret
cmd=${realcmd%/*}/$cmdname # basename could be an alias
akinomyoga marked this conversation as resolved.
Show resolved Hide resolved
fi

local -a dirs=()

Expand All @@ -2523,68 +2538,71 @@ __load_completion()
dirs+=(./completions)
fi

# 3) From bin directories extracted from $(realpath "$cmd") and PATH
local ret
_comp_realcommand "$1" && paths=("${ret%/*}") || paths=()
# 3) From bin directories extracted from path to command, and $PATH
[[ $realcmd ]] && paths=("${realcmd%/*}") || paths=()
_comp_split -aF : paths "$PATH"
for dir in "${paths[@]%/}"; do
if [[ -d $dir && $dir == ?*/@(bin|sbin) ]]; then
[[ $dir == ?*/@(bin|sbin) ]] &&
dirs+=("${dir%/*}/share/bash-completion/completions")
fi
done

# 4) From XDG_DATA_DIRS or system dirs (e.g. /usr/share, /usr/local/share):
# Completions in the system data dirs.
_comp_split -F : paths "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
dirs+=("${paths[@]/%//bash-completion/completions}")

local backslash=
if [[ $cmd == \\* ]]; then
cmd=${cmd:1}
# If we already have a completion for the "real" command, use it
$(complete -p "$cmd" 2>/dev/null || echo false) "\\$cmd" && return 0
backslash=\\
fi

# For loading 3rd party completions wrapped in shopt reset
# Set up default $IFS in case loaded completions depend on it,
# as well as for $compspec invocation below.
local IFS=$' \t\n'

for dir in "${dirs[@]}"; do
[[ -d $dir ]] || continue
for compfile in "$cmd" "$cmd.bash"; do
compfile="$dir/$compfile"
# Avoid trying to source dirs as long as we support bash < 4.3
# to avoid an fd leak; https://bugzilla.redhat.com/903540
if [[ -d $compfile ]]; then
# Do not warn with . or .. (especially the former is common)
[[ $compfile == */.?(.) ]] ||
echo "bash_completion: $compfile: is a directory" >&2
elif [[ -e $compfile ]] && . "$compfile"; then
[[ $backslash ]] && $(complete -p "$cmd") "\\$cmd"
return 0
# Look up and source
shift
local i prefix compspec
for prefix in "" _; do # Regular from all dirs first, then fallbacks
for i in ${!dirs[*]}; do
dir=${dirs[i]}
if [[ ! -d $dir ]]; then
unset -v 'dirs[i]'
continue
fi
for compfile in "$prefix$cmdname" "$prefix$cmdname.bash"; do
compfile="$dir/$compfile"
# Avoid trying to source dirs as long as we support bash < 4.3
# to avoid an fd leak; https://bugzilla.redhat.com/903540
if [[ -d $compfile ]]; then
# Do not warn with . or .. (especially the former is common)
[[ $compfile == */.?(.) ]] ||
echo "bash_completion: $compfile: is a directory" >&2
elif [[ -e $compfile ]] && . "$compfile" "$cmd" "$@"; then
# At least $cmd is expected to have a completion set when
# we return successfully; see if it already does
if compspec=$(complete -p "$cmd" 2>/dev/null); then
local -a extspecs=()
# $cmd is the case in which we do backslash processing
[[ $backslash ]] && extspecs+=("$backslash$cmd")
# If invoked without path, that one should be set, too
# ...but let's not overwrite an existing one, if any
[[ $origcmd != */* ]] &&
! complete -p "$origcmd" &>/dev/null &&
extspecs+=("$origcmd")
((${#extspecs[*]} != 0)) && $compspec "${extspecs[@]}"
return 0
fi
# If not, see if we got one for $cmdname
if compspec=$(complete -p "$cmdname" 2>/dev/null); then
scop marked this conversation as resolved.
Show resolved Hide resolved
# Use that for $cmd too, if we have a full path to it
[[ $cmd == /* ]] && $compspec "$cmd"
return 0
fi
# Nothing expected was set, continue lookup
fi
done
done
done

# Search fallback completions named "_$cmd"
for dir in "${dirs[@]}"; do
[[ -d $dir ]] || continue
compfile="$dir/_$cmd"
# Avoid trying to source dirs as long as we support bash < 4.3
# to avoid an fd leak; https://bugzilla.redhat.com/903540
if [[ -d $compfile ]]; then
# Do not warn with . or .. (especially the former is common)
[[ $compfile == */.?(.) ]] ||
echo "bash_completion: $compfile: is a directory" >&2
elif [[ -e $compfile ]] && . "$compfile" "$cmd"; then
[[ $backslash ]] && $(complete -p "$cmd") "\\$cmd"
return 0
fi
done

# Look up simple "xspec" completions
[[ -v _xspecs[$cmd] ]] &&
complete -F _filedir_xspec "$cmd" "$backslash$cmd" && return 0
[[ -v _xspecs[$cmdname] ]] &&
complete -F _filedir_xspec "$cmdname" "$backslash$cmdname" && return 0

return 1
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
echo 'cmd1: sourced from prefix1'
complete -C true "$1"
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
echo 'cmd2: sourced from prefix1'
complete -C true "$1"
1 change: 1 addition & 0 deletions test/fixtures/__load_completion/userdir1/completions/cmd1
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
echo 'cmd1: sourced from userdir1'
complete -C true "$1"
1 change: 1 addition & 0 deletions test/fixtures/__load_completion/userdir2/completions/cmd2
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
echo 'cmd2: sourced from userdir2'
complete -C true "$1"
14 changes: 14 additions & 0 deletions test/t/unit/test_unit_load_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,26 @@ def test_PATH_1(self, bash):
bash, "__load_completion cmd2", want_output=True
)
assert output.strip() == "cmd2: sourced from prefix1"
output = assert_bash_exec(
bash, "complete -p cmd2", want_output=True
)
assert " cmd2" in output
output = assert_bash_exec(
bash, 'complete -p "$PWD/prefix1/sbin/cmd2"', want_output=True
)
assert "/prefix1/sbin/cmd2" in output

def test_cmd_path_1(self, bash):
assert_bash_exec(bash, "complete -r cmd1 || :", want_output=None)
output = assert_bash_exec(
bash, "__load_completion prefix1/bin/cmd1", want_output=True
)
assert output.strip() == "cmd1: sourced from prefix1"
output = assert_bash_exec(
bash, 'complete -p "$PWD/prefix1/bin/cmd1"', want_output=True
)
assert "/prefix1/bin/cmd1" in output
assert_bash_exec(bash, "! complete -p cmd1", want_output=None)
output = assert_bash_exec(
bash, "__load_completion prefix1/sbin/cmd2", want_output=True
)
Expand Down