Skip to content

Commit da7aba4

Browse files
committed
feat(_comp_compgen): support -P prefix with auto-adjusted cur
The Bash builtin `compgen -P prefix ... -- "$cur"` prepends the prefix AFTER filtering completions using `$cur`. However, we usually want to filter completions with "$cur" starting with the prefix. To properly handle such a situation, one first needs to check if the current content of "$cur" is compatible. Then, one needs to modify $cur to remove the prefix part, generate completions, and prepends the prefix to the generated completions. This pattern is used frequently in the codebase, so it is good to handle it within `_comp_compgen`. This patch implements the option `-P` of `_comp_compgen`. When a non-empty string is specified to the `-P` option, it performs the necessary operations: the check and adjustment of $cur, the proper filtering by the prefix string, and prepending of the prefix string.
1 parent 6bd5e26 commit da7aba4

File tree

3 files changed

+118
-10
lines changed

3 files changed

+118
-10
lines changed

bash_completion

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ _comp_split()
439439
# @var[in] $?
440440
# @var[in] _var
441441
# @var[in] _append
442+
# @var[in] _upvars
442443
# @return original $?
443444
_comp_compgen__error_fallback()
444445
{
@@ -449,6 +450,10 @@ _comp_compgen__error_fallback()
449450
else
450451
eval -- "$_var=()"
451452
fi
453+
if ((${#_upvars[@]})); then
454+
_comp_unlocal "${_upvars[@]}"
455+
_upvars=()
456+
fi
452457
return "$_status"
453458
}
454459

@@ -478,6 +483,13 @@ _comp_compgen__error_fallback()
478483
# is ${cur-}.
479484
# -R The same as -c ''. Use raw outputs without filtering.
480485
# -C dir Evaluate compgen/generator in the specified directory.
486+
# -P pref Prepend the prefix to the generated completions. Unlike `compgen
487+
# -P prefix`, this prefix is subject to filtering by `cur`. When a
488+
# non-empty prefix is specified, first `cur` is tested whether it is
489+
# consistent with the prefix. Then, `cur` is reduced for the part
490+
# excluding the prefix, and the normal completion generation is
491+
# performed. Finally, the prefix is prepended to generated
492+
# completions.
481493
# @var[in,opt] cur Used as the default value of a prefix to filter the
482494
# completions.
483495
#
@@ -562,6 +574,7 @@ _comp_compgen()
562574
local _var=
563575
local _cur=${_comp_compgen__cur-${cur-}}
564576
local _dir=""
577+
local _prefix=""
565578
local _ifs=$' \t\n' _has_ifs=""
566579
local _icmd="" _xcmd=""
567580
local -a _upvars=()
@@ -572,7 +585,7 @@ _comp_compgen()
572585
shopt -u nocasematch
573586
fi
574587
local OPTIND=1 OPTARG="" OPTERR=0 _opt
575-
while getopts ':av:U:Rc:C:lF:i:x:' _opt "$@"; do
588+
while getopts ':av:U:Rc:C:P:lF:i:x:' _opt "$@"; do
576589
case $_opt in
577590
a) _append=set ;;
578591
v)
@@ -601,6 +614,7 @@ _comp_compgen()
601614
fi
602615
_dir=$OPTARG
603616
;;
617+
P) _prefix=$OPTARG ;;
604618
l) _has_ifs=set _ifs=$'\n' ;;
605619
F) _has_ifs=set _ifs=$OPTARG ;;
606620
[ix])
@@ -636,6 +650,19 @@ _comp_compgen()
636650
[[ $_append ]] || _append=${_comp_compgen__append-}
637651
fi
638652

653+
local _prefix_fail=""
654+
if [[ $_prefix ]]; then
655+
if [[ $_cur == "$_prefix"* ]]; then
656+
_cur=${_cur#"$_prefix"}
657+
elif [[ $_prefix == "$_cur"* ]]; then
658+
_cur=""
659+
else
660+
# No completions are generated because the current word does not match
661+
# the prefix.
662+
_prefix_fail=set
663+
fi
664+
fi
665+
639666
if [[ $1 != -* ]]; then
640667
# usage: _comp_compgen [options] NAME args
641668
if [[ $_has_ifs ]]; then
@@ -657,6 +684,11 @@ _comp_compgen()
657684
fi
658685
shift
659686

687+
if [[ $_prefix_fail ]]; then
688+
_comp_compgen__error_fallback
689+
return 1
690+
fi
691+
660692
_comp_compgen__call_generator "$@"
661693
else
662694
# usage: _comp_compgen [options] -- [compgen_options]
@@ -680,6 +712,11 @@ _comp_compgen()
680712
return 2
681713
fi
682714

715+
if [[ $_prefix_fail ]]; then
716+
_comp_compgen__error_fallback
717+
return 1
718+
fi
719+
683720
_comp_compgen__call_builtin "$@"
684721
fi
685722
}
@@ -694,8 +731,6 @@ _comp_compgen()
694731
# @var[in] _var
695732
_comp_compgen__call_generator()
696733
{
697-
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
698-
699734
if [[ $_dir ]]; then
700735
local _original_pwd=$PWD
701736
local PWD=${PWD-} OLDPWD=${OLDPWD-}
@@ -708,14 +743,28 @@ _comp_compgen__call_generator()
708743
}
709744
fi
710745

746+
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
747+
711748
local _comp_compgen__append=$_append
712749
local _comp_compgen__var=$_var
713750
local _comp_compgen__cur=$_cur cur=$_cur
751+
if [[ $_prefix ]]; then
752+
local -a tmp=()
753+
local _comp_compgen__var=tmp
754+
local _comp_compgen__append=""
755+
fi
714756
# Note: we use $1 as a part of a function name, and we use $2... as
715757
# arguments to the function if any.
716758
# shellcheck disable=SC2145
717759
"${_generator[@]}" "$@"
718760
local _status=$?
761+
if [[ $_prefix ]]; then
762+
local _i
763+
for _i in "${!tmp[@]}"; do
764+
tmp[_i]=$_prefix${tmp[_i]}
765+
done
766+
_comp_compgen -RU tmp ${_append:+-a} -v "$_var" -- -W '"${tmp[@]}"'
767+
fi
719768

720769
# Go back to the original directory.
721770
# Note: Failure of this line results in the change of the current
@@ -770,6 +819,13 @@ if ((BASH_VERSINFO[0] > 5 || BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3)); t
770819

771820
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
772821
((${#_result[@]})) || return
822+
823+
if [[ $_prefix ]]; then
824+
local _i
825+
for _i in "${!_result[@]}"; do
826+
_result[_i]=$_prefix${_result[_i]}
827+
done
828+
fi
773829
if [[ $_append ]]; then
774830
eval -- "$_var+=(\"\${_result[@]}\")"
775831
else
@@ -794,7 +850,13 @@ else
794850
}
795851

796852
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
797-
_comp_split -l ${_append:+-a} "$_var" "$_result"
853+
854+
if [[ $_prefix ]]; then
855+
_comp_split -l ${_append:+-a} "$_var" \
856+
"$(IFS=$'\n' compgen -W '$_result' -P "$_prefix")"
857+
else
858+
_comp_split -l ${_append:+-a} "$_var" "$_result"
859+
fi
798860
}
799861
fi
800862

doc/api-and-naming.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,12 @@ concept is later extended to also mean "eXported".
143143

144144
The generator functions, which have names of the form `_comp_compgen_NAME`, are
145145
used to generate completion candidates. A generator function is supposed to be
146-
called by `_comp_compgen [OPTS] NAME ARGS` where `OPTS = -aRl|-v var|-c cur|-C
147-
dir|-F sep` are the options to modify the behavior (see the code comment of
148-
`_comp_compgen` for details). When there are no `opts`, the generator function
149-
is supposed to be directly called as `_comp_compgen_NAME ARGS`. The result is
150-
stored in the target variable (which is `COMPREPLY` by default but can be
151-
specified by `-v var` in `OPTS`).
146+
called by `_comp_compgen [OPTS] NAME ARGS` where `OPTS = -aR|-v var|-c cur|-C
147+
dir|-U var|-P pref` are the options to modify the behavior (see the code
148+
comment of `_comp_compgen` for details). When there are no `opts`, the
149+
generator function is supposed to be directly called as `_comp_compgen_NAME
150+
ARGS`. The result is stored in the target variable (which is `COMPREPLY` by
151+
default but can be specified by `-v var` in `OPTS`).
152152

153153
### Implementing a generator function
154154

test/t/unit/test_unit_compgen.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,49 @@ def test_9_inherit_a(self, bash, functions):
172172
bash, "_comp__test_compgen gen9", want_output=True
173173
)
174174
assert output.strip() == "<11><11>"
175+
176+
def test_10_option_P_unmatching_prefix(self, bash, functions):
177+
output = assert_bash_exec(
178+
bash,
179+
"_comp__test_compgen -c 'x' -P 'prefix,' -- -W 'alpha apple beta lemon'",
180+
want_output=True,
181+
)
182+
assert output.strip() == ""
183+
184+
def test_10_option_P_incomplete_prefix(self, bash, functions):
185+
output = assert_bash_exec(
186+
bash,
187+
"_comp__test_compgen -c 'pre' -P 'prefix,' -- -W 'alpha apple beta lemon'",
188+
want_output=True,
189+
)
190+
assert (
191+
output.strip()
192+
== "<prefix,alpha><prefix,apple><prefix,beta><prefix,lemon>"
193+
)
194+
195+
def test_10_option_P_exact_prefix(self, bash, functions):
196+
output = assert_bash_exec(
197+
bash,
198+
"_comp__test_compgen -c 'prefix,' -P 'prefix,' -- -W 'alpha apple beta lemon'",
199+
want_output=True,
200+
)
201+
assert (
202+
output.strip()
203+
== "<prefix,alpha><prefix,apple><prefix,beta><prefix,lemon>"
204+
)
205+
206+
def test_10_option_P_starts_with_prefix(self, bash, functions):
207+
output = assert_bash_exec(
208+
bash,
209+
"_comp__test_compgen -c 'prefix,a' -P 'prefix,' -- -W 'alpha apple beta lemon'",
210+
want_output=True,
211+
)
212+
assert output.strip() == "<prefix,alpha><prefix,apple>"
213+
214+
def test_10_option_P_no_match(self, bash, functions):
215+
output = assert_bash_exec(
216+
bash,
217+
"_comp__test_compgen -c 'prefix,x' -P 'prefix,' -- -W 'alpha apple beta lemon'",
218+
want_output=True,
219+
)
220+
assert output.strip() == ""

0 commit comments

Comments
 (0)