Skip to content

Commit 6da0ae8

Browse files
akinomyogascop
andcommitted
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. Co-authored-by: Ville Skyttä <[email protected]>
1 parent 69c6a51 commit 6da0ae8

File tree

3 files changed

+141
-33
lines changed

3 files changed

+141
-33
lines changed

bash_completion

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ _comp_split()
441441
# @var[in] $?
442442
# @var[in] _var
443443
# @var[in] _append
444+
# @var[in] _upvars
444445
# @return original $?
445446
_comp_compgen__error_fallback()
446447
{
@@ -451,35 +452,46 @@ _comp_compgen__error_fallback()
451452
else
452453
eval -- "$_var=()"
453454
fi
455+
if ((${#_upvars[@]})); then
456+
_comp_unlocal "${_upvars[@]}"
457+
_upvars=()
458+
fi
454459
return "$_status"
455460
}
456461

457462
# Provide a common interface to generate completion candidates in COMPREPLY or
458463
# in a specified array.
459464
# OPTIONS
460-
# -a Append to the array
461-
# -v arr Store the results to the array ARR. The default is `COMPREPLY`.
462-
# The array name should not start with an underscores "_", which is
463-
# internally used. The array name should not be any of "cur", "IFS"
464-
# or "OPT{IND,ARG,ERR}".
465-
# -U var Unlocalize VAR before performing the assignments. This option can
466-
# be specified multiple times to register multiple variables. This
467-
# option is supposed to be used in implementing a generator (G1) when
468-
# G1 defines a local variable name that does not start with `_`. In
469-
# such a case, when the target variable specified to G1 by `-v VAR1`
470-
# conflicts with the local variable, the assignment to the target
471-
# variable fails to propagate outside G1. To avoid such a situation,
472-
# G1 can call `_comp_compgen` with `-U VAR` to unlocalize `VAR`
473-
# before accessing the target variable. For a builtin compgen call
474-
# (i.e., _comp_compgen [options] -- options), VAR is unlocalized
475-
# after calling the builtin `compgen` but before assigning results to
476-
# the target array. For a generator call (i.e., _comp_compgen
477-
# [options] G2 ...), VAR is unlocalized before calling the child
478-
# generator function `_comp_compgen_G2`.
479-
# -c cur Set a word used as a prefix to filter the completions. The default
480-
# is ${cur-}.
481-
# -R The same as -c ''. Use raw outputs without filtering.
482-
# -C dir Evaluate compgen/generator in the specified directory.
465+
# -a Append to the array
466+
# -v arr Store the results to the array ARR. The default is `COMPREPLY`.
467+
# The array name should not start with an underscores "_", which is
468+
# internally used. The array name should not be any of "cur",
469+
# "IFS" or "OPT{IND,ARG,ERR}".
470+
# -U var Unlocalize VAR before performing the assignments. This option
471+
# can be specified multiple times to register multiple variables.
472+
# This option is supposed to be used in implementing a generator
473+
# (G1) when G1 defines a local variable name that does not start
474+
# with `_`. In such a case, when the target variable specified to
475+
# G1 by `-v VAR1` conflicts with the local variable, the assignment
476+
# to the target variable fails to propagate outside G1. To avoid
477+
# such a situation, G1 can call `_comp_compgen` with `-U VAR` to
478+
# unlocalize `VAR` before accessing the target variable. For a
479+
# builtin compgen call (i.e., _comp_compgen [options] -- options),
480+
# VAR is unlocalized after calling the builtin `compgen` but before
481+
# assigning results to the target array. For a generator call
482+
# (i.e., _comp_compgen [options] G2 ...), VAR is unlocalized before
483+
# calling the child generator function `_comp_compgen_G2`.
484+
# -c cur Set a word used as a prefix to filter the completions. The
485+
# default is ${cur-}.
486+
# -R The same as -c ''. Use raw outputs without filtering.
487+
# -C dir Evaluate compgen/generator in the specified directory.
488+
# -P prefix Prepend the prefix to the generated completions. Unlike `compgen
489+
# -P prefix`, this prefix is subject to filtering by `cur`. When a
490+
# non-empty prefix is specified, first `cur` is tested whether it
491+
# is consistent with the prefix. Then, `cur` is reduced for the
492+
# part excluding the prefix, and the normal completion generation
493+
# is performed. Finally, the prefix is prepended to generated
494+
# completions.
483495
# @var[in,opt] cur Used as the default value of a prefix to filter the
484496
# completions.
485497
#
@@ -564,6 +576,7 @@ _comp_compgen()
564576
local _var=
565577
local _cur=${_comp_compgen__cur-${cur-}}
566578
local _dir=""
579+
local _prefix=""
567580
local _ifs=$' \t\n' _has_ifs=""
568581
local _icmd="" _xcmd=""
569582
local -a _upvars=()
@@ -574,7 +587,7 @@ _comp_compgen()
574587
shopt -u nocasematch
575588
fi
576589
local OPTIND=1 OPTARG="" OPTERR=0 _opt
577-
while getopts ':av:U:Rc:C:lF:i:x:' _opt "$@"; do
590+
while getopts ':av:U:Rc:C:P:lF:i:x:' _opt "$@"; do
578591
case $_opt in
579592
a) _append=set ;;
580593
v)
@@ -603,6 +616,7 @@ _comp_compgen()
603616
fi
604617
_dir=$OPTARG
605618
;;
619+
P) _prefix=$OPTARG ;;
606620
l) _has_ifs=set _ifs=$'\n' ;;
607621
F) _has_ifs=set _ifs=$OPTARG ;;
608622
[ix])
@@ -638,6 +652,19 @@ _comp_compgen()
638652
[[ $_append ]] || _append=${_comp_compgen__append-}
639653
fi
640654

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

689+
if [[ $_prefix_fail ]]; then
690+
_comp_compgen__error_fallback
691+
return 1
692+
fi
693+
662694
_comp_compgen__call_generator "$@"
663695
else
664696
# usage: _comp_compgen [options] -- [compgen_options]
@@ -682,6 +714,11 @@ _comp_compgen()
682714
return 2
683715
fi
684716

717+
if [[ $_prefix_fail ]]; then
718+
_comp_compgen__error_fallback
719+
return 1
720+
fi
721+
685722
_comp_compgen__call_builtin "$@"
686723
fi
687724
}
@@ -696,8 +733,6 @@ _comp_compgen()
696733
# @var[in] _var
697734
_comp_compgen__call_generator()
698735
{
699-
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
700-
701736
if [[ $_dir ]]; then
702737
local _original_pwd=$PWD
703738
local PWD=${PWD-} OLDPWD=${OLDPWD-}
@@ -710,14 +745,28 @@ _comp_compgen__call_generator()
710745
}
711746
fi
712747

748+
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
749+
713750
local _comp_compgen__append=$_append
714751
local _comp_compgen__var=$_var
715752
local _comp_compgen__cur=$_cur cur=$_cur
753+
if [[ $_prefix ]]; then
754+
local -a tmp=()
755+
local _comp_compgen__var=tmp
756+
local _comp_compgen__append=""
757+
fi
716758
# Note: we use $1 as a part of a function name, and we use $2... as
717759
# arguments to the function if any.
718760
# shellcheck disable=SC2145
719761
"${_generator[@]}" "$@"
720762
local _status=$?
763+
if [[ $_prefix ]]; then
764+
local _i
765+
for _i in "${!tmp[@]}"; do
766+
tmp[_i]=$_prefix${tmp[_i]}
767+
done
768+
_comp_compgen -RU tmp ${_append:+-a} -v "$_var" -- -W '"${tmp[@]}"'
769+
fi
721770

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

773822
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
774823
((${#_result[@]})) || return
824+
825+
if [[ $_prefix ]]; then
826+
local _i
827+
for _i in "${!_result[@]}"; do
828+
_result[_i]=$_prefix${_result[_i]}
829+
done
830+
fi
775831
if [[ $_append ]]; then
776832
eval -- "$_var+=(\"\${_result[@]}\")"
777833
else
@@ -796,7 +852,13 @@ else
796852
}
797853

798854
((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}"
799-
_comp_split -l ${_append:+-a} "$_var" "$_result"
855+
856+
if [[ $_prefix ]]; then
857+
_comp_split -l ${_append:+-a} "$_var" \
858+
"$(IFS=$'\n' compgen -W '$_result' -P "$_prefix")"
859+
else
860+
_comp_split -l ${_append:+-a} "$_var" "$_result"
861+
fi
800862
}
801863
fi
802864

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 prefix` 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)