Skip to content

Commit 542bf73

Browse files
akinomyogascop
andcommitted
feat: add _comp_compgen_split
Co-authored-by: Ville Skyttä <[email protected]>
1 parent 99fdcbe commit 542bf73

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

bash_completion

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,46 @@ _comp_compgen_set()
633633
eval -- "$_var${_append:++}=(\"\$@\")"
634634
}
635635

636+
# Simply split the text and generate completions. This function should be used
637+
# instead of `_comp_compgen -- -W "$(command)"`, which is vulnerable because
638+
# option -W evaluates the shell expansions included in the option argument.
639+
# Options:
640+
# -F sep Specify the separators. The default is $' \t\n'
641+
# -l The same as -F $'\n'
642+
# -X arg The same as the compgen option -X.
643+
# -S arg The same as the compgen option -S.
644+
# -P arg The same as the compgen option -P.
645+
# -o arg The same as the compgen option -o.
646+
# @param $1 String to split
647+
# @since 2.12
648+
_comp_compgen_split()
649+
{
650+
local _ifs=$' \t\n'
651+
local -a _compgen_options=()
652+
653+
local OPTIND=1 OPTARG="" OPTERR=0 _opt
654+
while getopts ':lF:X:S:P:o:' _opt "$@"; do
655+
case $_opt in
656+
l) _ifs=$'\n' ;;
657+
F) _ifs=$OPTARG ;;
658+
[XSPo]) _compgen_options+=("-$_opt" "$OPTARG") ;;
659+
*)
660+
printf 'bash_completion: usage: %s [-l|-F sep] [--] str\n' "$FUNCNAME" >&2
661+
return 2
662+
;;
663+
esac
664+
done
665+
shift "$((OPTIND - 1))"
666+
if (($# != 1)); then
667+
printf 'bash_completion: %s: unexpected number of arguments.\n' "$FUNCNAME" >&2
668+
printf 'usage: %s [-l|-F sep] [--] str' "$FUNCNAME" >&2
669+
return 2
670+
fi
671+
672+
local _split_input=$1
673+
_comp_compgen -F "$_ifs" -- "${_compgen_options[@]}" -W '$_split_input'
674+
}
675+
636676
# Check if the argument looks like a path.
637677
# @param $1 thing to check
638678
# @return True (0) if it does, False (> 0) otherwise
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import pytest
2+
3+
from conftest import assert_bash_exec
4+
5+
6+
@pytest.mark.bashcomp(cmd=None)
7+
class TestUtilCompgenSplit:
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_compgen() { local -a arr=(00); _comp_compgen -v arr "$@"; _comp__test_dump; }',
17+
)
18+
19+
assert_bash_exec(
20+
bash,
21+
"_comp__test_cmd1() { echo foo bar; echo baz; }",
22+
)
23+
assert_bash_exec(
24+
bash,
25+
'_comp__test_attack() { echo "\\$(echo should_not_run >&2)"; }',
26+
)
27+
28+
def test_1_basic(self, bash, functions):
29+
output = assert_bash_exec(
30+
bash,
31+
'_comp__test_compgen split -- "$(_comp__test_cmd1)"',
32+
want_output=True,
33+
)
34+
assert output.strip() == "<foo><bar><baz>"
35+
36+
def test_2_attack(self, bash, functions):
37+
output = assert_bash_exec(
38+
bash,
39+
'_comp__test_compgen split -- "$(_comp__test_attack)"',
40+
want_output=True,
41+
)
42+
assert output.strip() == "<$(echo><should_not_run><>&2)>"
43+
44+
def test_3_sep1(self, bash, functions):
45+
output = assert_bash_exec(
46+
bash,
47+
'_comp__test_compgen split -l -- "$(_comp__test_cmd1)"',
48+
want_output=True,
49+
)
50+
assert output.strip() == "<foo bar><baz>"
51+
52+
def test_3_sep2(self, bash, functions):
53+
output = assert_bash_exec(
54+
bash,
55+
"_comp__test_compgen split -F $'b\\n' -- \"$(_comp__test_cmd1)\"",
56+
want_output=True,
57+
)
58+
assert output.strip() == "<foo ><ar><az>"
59+
60+
def test_4_optionX(self, bash, functions):
61+
output = assert_bash_exec(
62+
bash,
63+
'_comp__test_compgen split -X bar -- "$(_comp__test_cmd1)"',
64+
want_output=True,
65+
)
66+
assert output.strip() == "<foo><baz>"
67+
68+
def test_4_optionS(self, bash, functions):
69+
output = assert_bash_exec(
70+
bash,
71+
'_comp__test_compgen split -S .txt -- "$(_comp__test_cmd1)"',
72+
want_output=True,
73+
)
74+
assert output.strip() == "<foo.txt><bar.txt><baz.txt>"
75+
76+
def test_4_optionP(self, bash, functions):
77+
output = assert_bash_exec(
78+
bash,
79+
'_comp__test_compgen split -P /tmp/ -- "$(_comp__test_cmd1)"',
80+
want_output=True,
81+
)
82+
assert output.strip() == "</tmp/foo></tmp/bar></tmp/baz>"
83+
84+
def test_4_optionPS(self, bash, functions):
85+
output = assert_bash_exec(
86+
bash,
87+
'_comp__test_compgen split -P [ -S ] -- "$(_comp__test_cmd1)"',
88+
want_output=True,
89+
)
90+
assert output.strip() == "<[foo]><[bar]><[baz]>"
91+
92+
def test_5_empty(self, bash, functions):
93+
output = assert_bash_exec(
94+
bash, '_comp__test_compgen split -- ""', want_output=True
95+
)
96+
assert output.strip() == ""
97+
98+
def test_5_empty2(self, bash, functions):
99+
output = assert_bash_exec(
100+
bash, '_comp__test_compgen split -- " "', want_output=True
101+
)
102+
assert output.strip() == ""

0 commit comments

Comments
 (0)