Skip to content

Commit 28c4ed9

Browse files
akinomyogascop
andcommitted
feat(_comp_expand_glob): add utility to safely expand pathnames
Co-authored-by: Ville Skyttä <[email protected]>
1 parent 6bc91ed commit 28c4ed9

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

bash_completion

+49
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,55 @@ _upvars()
260260
done
261261
}
262262

263+
# Get the list of filenames that match with the specified glob pattern.
264+
# This function does the globbing in a controlled environment, avoiding
265+
# interference from user's shell options/settings or environment variables.
266+
# @param $1 array_name Array name
267+
# The array name should not start with the double underscores "__". The
268+
# array name should not be "GLOBIGNORE".
269+
# @param $2 pattern Pattern string to be evaluated.
270+
# This pattern string will be evaluated using "eval", so brace expansions,
271+
# parameter expansions, command substitutions, and other expansions will be
272+
# processed. The user-provided strings should not be directly specified to
273+
# this argument.
274+
_comp_expand_glob()
275+
{
276+
if (($# != 2)); then
277+
printf 'bash-completion: %s: unexpected number of arguments\n' "$FUNCNAME" >&2
278+
printf 'usage: %s ARRAY_NAME PATTERN\n' "$FUNCNAME" >&2
279+
return 2
280+
elif [[ $1 == @(GLOBIGNORE|__*|*[^_a-zA-Z0-9]*|[0-9]*|'') ]]; then
281+
printf 'bash-completion: %s: invalid array name "%s"\n' "$FUNCNAME" "$1" >&2
282+
return 2
283+
fi
284+
285+
# Save and adjust the settings.
286+
local __original_opts=$SHELLOPTS:$BASHOPTS
287+
set +o noglob
288+
shopt -s nullglob
289+
shopt -u failglob dotglob
290+
291+
# Also the user's GLOBIGNORE may affect the result of pathname expansions.
292+
local GLOBIGNORE=
293+
294+
eval -- "$1=()" # a fallback in case that the next line fails.
295+
eval -- "$1=($2)"
296+
297+
# Restore the settings. Note: Changing GLOBIGNORE affects the state of
298+
# "shopt -q dotglob", so we need to explicitly restore the original state
299+
# of "shopt -q dotglob".
300+
_comp_unlocal GLOBIGNORE
301+
if [[ :$__original_opts: == *:dotglob:* ]]; then
302+
shopt -s dotglob
303+
else
304+
shopt -u dotglob
305+
fi
306+
[[ :$__original_opts: == *:nullglob:* ]] || shopt -u nullglob
307+
[[ :$__original_opts: == *:failglob:* ]] && shopt -s failglob
308+
[[ :$__original_opts: == *:noglob:* ]] && set -o noglob
309+
return 0
310+
}
311+
263312
# Reassemble command line words, excluding specified characters from the
264313
# list of word completion separators (COMP_WORDBREAKS).
265314
# @param $1 chars Characters out of $COMP_WORDBREAKS which should

test/t/unit/test_unit_expand_glob.py

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import pytest
2+
3+
from conftest import assert_bash_exec, bash_env_saved
4+
5+
6+
@pytest.mark.bashcomp(
7+
cmd=None,
8+
cwd="_filedir",
9+
ignore_env=r"^\+(my_array=|declare -f dump_array$)",
10+
)
11+
class TestExpandGlob:
12+
def test_match_all(self, bash):
13+
assert_bash_exec(
14+
bash,
15+
"dump_array() { ((${#my_array[@]})) && printf '<%s>' \"${my_array[@]}\"; echo; }",
16+
)
17+
output = assert_bash_exec(
18+
bash,
19+
"LC_ALL= LC_COLLATE=C _comp_expand_glob my_array '*';dump_array",
20+
want_output=True,
21+
)
22+
assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé><brackets><ext>"
23+
24+
def test_match_pattern(self, bash):
25+
output = assert_bash_exec(
26+
bash,
27+
"LC_ALL= LC_COLLATE=C _comp_expand_glob my_array 'a*';dump_array",
28+
want_output=True,
29+
)
30+
assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé>"
31+
32+
def test_match_unmatched(self, bash):
33+
output = assert_bash_exec(
34+
bash,
35+
"_comp_expand_glob my_array 'unmatched-*';dump_array",
36+
want_output=True,
37+
)
38+
assert output.strip() == ""
39+
40+
def test_match_multiple_words(self, bash):
41+
output = assert_bash_exec(
42+
bash,
43+
"_comp_expand_glob my_array 'b* e*';dump_array",
44+
want_output=True,
45+
)
46+
assert output.strip() == "<brackets><ext>"
47+
48+
def test_match_brace_expansion(self, bash):
49+
output = assert_bash_exec(
50+
bash,
51+
"_comp_expand_glob my_array 'brac{ket,unmatched}*';dump_array",
52+
want_output=True,
53+
)
54+
assert output.strip() == "<brackets>"
55+
56+
def test_protect_from_noglob(self, bash):
57+
with bash_env_saved(bash) as bash_env:
58+
bash_env.set("noglob", True)
59+
output = assert_bash_exec(
60+
bash,
61+
"LC_ALL= LC_COLLATE=C _comp_expand_glob my_array 'a*';dump_array",
62+
want_output=True,
63+
)
64+
assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé>"
65+
66+
def test_protect_from_failglob(self, bash):
67+
with bash_env_saved(bash) as bash_env:
68+
bash_env.shopt("failglob", True)
69+
output = assert_bash_exec(
70+
bash,
71+
"_comp_expand_glob my_array 'unmatched-*';dump_array",
72+
want_output=True,
73+
)
74+
assert output.strip() == ""
75+
76+
def test_protect_from_nullglob(self, bash):
77+
with bash_env_saved(bash) as bash_env:
78+
bash_env.shopt("nullglob", False)
79+
output = assert_bash_exec(
80+
bash,
81+
"_comp_expand_glob my_array 'unmatched-*';dump_array",
82+
want_output=True,
83+
)
84+
assert output.strip() == ""
85+
86+
def test_protect_from_dotglob(self, bash):
87+
with bash_env_saved(bash) as bash_env:
88+
bash_env.shopt("dotglob", True)
89+
output = assert_bash_exec(
90+
bash,
91+
"_comp_expand_glob my_array 'ext/foo/*';dump_array",
92+
want_output=True,
93+
)
94+
assert output.strip() == ""
95+
96+
def test_protect_from_GLOBIGNORE(self, bash):
97+
with bash_env_saved(bash) as bash_env:
98+
# Note: dotglob is changed by GLOBIGNORE
99+
bash_env.save_shopt("dotglob")
100+
bash_env.write_variable("GLOBIGNORE", "*")
101+
output = assert_bash_exec(
102+
bash,
103+
"LC_ALL= LC_COLLATE=C _comp_expand_glob my_array 'a*';dump_array",
104+
want_output=True,
105+
)
106+
assert output.strip() == "<a b><a$b><a&b><a'b><ab><aé>"

0 commit comments

Comments
 (0)