Skip to content

Commit 652f9e4

Browse files
akinomyogascop
andcommitted
feat(_comp_compgen): add helper function to call compgen
Co-authored-by: Ville Skyttä <[email protected]>
1 parent 7e2692a commit 652f9e4

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed

bash_completion

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,79 @@ _comp_split()
381381
((_new_size > _old_size))
382382
}
383383

384+
# Call `compgen` with the specified arguments and store the results in the
385+
# specified array.
386+
# Usage: _comp_compgen [-al] [-F sep] ARRAY args... [-- cur]
387+
# This function essentially performs ARRAY=($(compgen args...)) but properly
388+
# handles shell options, IFS, etc. using _comp_split. This function is
389+
# equivalent to `_comp_split [-a] -F sep ARRAY "$(compgen args...)"`, but this
390+
# pattern is frequent in the codebase and is good to separate out as a function
391+
# for the possible future implementation change.
392+
# OPTIONS
393+
# -a Append to the array
394+
# -F sep Set a set of separator characters (used as IFS in evaluating
395+
# `compgen'). The default separator is $' \t\n'. Note that this is
396+
# not the set of separators to delimit output of `compgen', but the
397+
# separators in evaluating the expansions of `-W '...'`, etc. The
398+
# delimiter of the output of `compgen` is always a newline.
399+
# -l The same as -F $'\n'
400+
# @param $1 array_name The array name
401+
# The array name should not start with an underscores "_", which is
402+
# internally used. The array name should not be either "IFS" or
403+
# "OPT{IND,ARG,ERR}".
404+
# @param $2... args The arguments that are passed to compgen
405+
# Note: References to positional parameters $1, $2, ... (such as -W '$1')
406+
# will not work as expected because these reference the arguments of
407+
# `_comp_compgen' instead of those of the caller function. When there are
408+
# needs to reference them, save the arguments to an array and reference the
409+
# array instead.
410+
_comp_compgen()
411+
{
412+
local _append=false IFS=$' \t\n'
413+
local -a _split_options=(-l)
414+
415+
local OPTIND=1 OPTARG="" OPTERR=0 _opt
416+
while getopts ':alF:' _opt "$@"; do
417+
case $_opt in
418+
a) _append=true _split_options+=(-a) ;;
419+
l) IFS=$'\n' ;;
420+
F) IFS=$OPTARG ;;
421+
*)
422+
echo "bash_completion: $FUNCNAME: usage error" >&2
423+
return 2
424+
;;
425+
esac
426+
done
427+
shift "$((OPTIND - 1))"
428+
if (($# < 2)); then
429+
printf '%s\n' "bash_completion: $FUNCNAME: unexpected number of arguments." >&2
430+
printf '%s\n' "usage: $FUNCNAME [-al] [-F SEP] ARRAY_NAME ARGS... [-- CUR]" >&2
431+
return 2
432+
elif [[ $1 == @(*[^_a-zA-Z0-9]*|[0-9]*|''|_*|IFS|OPTIND|OPTARG|OPTERR) ]]; then
433+
printf '%s\n' "bash_completion: $FUNCNAME: invalid array name \`$1'." >&2
434+
return 2
435+
elif [[ ${*:2} == *\$[0-9]* || ${*:2} == *\$\{[0-9]* ]]; then
436+
# Note: extglob *\$?(\{)[0-9]* can be extremely slow when the string
437+
# "${*:2}" becomes longer, so we test \$[0-9] and \$\{[0-9] separately.
438+
printf '%s\n' "bash_completion: $FUNCNAME: positional parameter \$1, \$2, ... do not work inside this function." >&2
439+
return 2
440+
fi
441+
442+
local _result
443+
_result=$(compgen "${@:2}") || {
444+
local _status=$?
445+
if "$_append"; then
446+
# make sure existence of variable
447+
eval -- "$1+=()"
448+
else
449+
eval -- "$1=()"
450+
fi
451+
return "$_status"
452+
}
453+
454+
_comp_split "${_split_options[@]}" "$1" "$_result"
455+
}
456+
384457
# Check if the argument looks like a path.
385458
# @param $1 thing to check
386459
# @return True (0) if it does, False (> 0) otherwise

test/t/unit/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
EXTRA_DIST = \
22
test_unit_command_offset.py \
3+
test_unit_compgen.py \
34
test_unit_count_args.py \
45
test_unit_deprecate_func.py \
56
test_unit_dequote.py \

test/t/unit/test_unit_compgen.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import pytest
2+
3+
from conftest import assert_bash_exec, bash_env_saved
4+
5+
6+
@pytest.mark.bashcomp(cmd=None)
7+
class TestUtilCompgen:
8+
@pytest.fixture
9+
def functions(self, bash):
10+
assert_bash_exec(
11+
bash,
12+
"_comp__test_dump() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }",
13+
)
14+
assert_bash_exec(
15+
bash,
16+
'_comp__test_words() { local -a arr=(00) input; input=("${@:1:$#-1}"); _comp_compgen arr -W \'${input[@]+"${input[@]}"}\' -- "${@:$#}"; _comp__test_dump; }',
17+
)
18+
assert_bash_exec(
19+
bash,
20+
'_comp__test_words_ifs() { local -a arr=(00); local input=$2; _comp_compgen -F "$1" arr -W \'$input\' -- "${@:$#}"; _comp__test_dump; }',
21+
)
22+
23+
def test_1_basic(self, bash, functions):
24+
output = assert_bash_exec(
25+
bash, "_comp__test_words 12 34 56 ''", want_output=True
26+
)
27+
assert output.strip() == "<12><34><56>"
28+
29+
def test_2_space(self, bash, functions):
30+
output = assert_bash_exec(
31+
bash,
32+
"_comp__test_words $'a b' $'c d\\t' ' e ' $'\\tf\\t' ''",
33+
want_output=True,
34+
)
35+
assert output.strip() == "<a b><c d\t>< e ><\tf\t>"
36+
37+
def test_2_IFS(self, bash, functions):
38+
with bash_env_saved(bash) as bash_env:
39+
bash_env.write_variable("IFS", "34")
40+
output = assert_bash_exec(
41+
bash, "_comp__test_words 12 34 56 ''", want_output=True
42+
)
43+
assert output.strip() == "<12><34><56>"
44+
45+
def test_3_glob(self, bash, functions):
46+
output = assert_bash_exec(
47+
bash,
48+
"_comp__test_words '*' '[a-z]*' '[a][b][c]' ''",
49+
want_output=True,
50+
)
51+
assert output.strip() == "<*><[a-z]*><[a][b][c]>"
52+
53+
def test_3_failglob(self, bash, functions):
54+
with bash_env_saved(bash) as bash_env:
55+
bash_env.shopt("failglob", True)
56+
output = assert_bash_exec(
57+
bash,
58+
"_comp__test_words '*' '[a-z]*' '[a][b][c]' ''",
59+
want_output=True,
60+
)
61+
assert output.strip() == "<*><[a-z]*><[a][b][c]>"
62+
63+
def test_3_nullglob(self, bash, functions):
64+
with bash_env_saved(bash) as bash_env:
65+
bash_env.shopt("nullglob", True)
66+
output = assert_bash_exec(
67+
bash,
68+
"_comp__test_words '*' '[a-z]*' '[a][b][c]' ''",
69+
want_output=True,
70+
)
71+
assert output.strip() == "<*><[a-z]*><[a][b][c]>"
72+
73+
def test_4_empty(self, bash, functions):
74+
output = assert_bash_exec(
75+
bash, "_comp__test_words ''", want_output=True
76+
)
77+
assert output.strip() == ""
78+
79+
def test_5_option_F(self, bash, functions):
80+
output = assert_bash_exec(
81+
bash,
82+
"_comp__test_words_ifs '25' ' 123 456 555 ' ''",
83+
want_output=True,
84+
)
85+
assert output.strip() == "< 1><3 4><6 >< >"

0 commit comments

Comments
 (0)