Skip to content

Commit a07e2a2

Browse files
authored
Merge pull request #912 from akinomyoga/refactor-funcname-3
refactor: rename `_quote_readline_by_ref => _comp_quote_compgen`
2 parents 299551a + 642f899 commit a07e2a2

File tree

5 files changed

+217
-146
lines changed

5 files changed

+217
-146
lines changed

bash_completion

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,6 @@ _comp_quote()
135135
ret=\'${1//\'/\'\\\'\'}\'
136136
}
137137

138-
# @see _quote_readline_by_ref()
139-
quote_readline()
140-
{
141-
local ret
142-
_quote_readline_by_ref "$1" ret
143-
printf %s "$ret"
144-
} # quote_readline()
145-
146138
# shellcheck disable=SC1003
147139
_comp_dequote__initialize()
148140
{
@@ -624,7 +616,7 @@ __ltrim_colon_completions()
624616

625617
# This function quotes the argument in a way so that readline dequoting
626618
# results in the original argument. This is necessary for at least
627-
# `compgen' which requires its arguments quoted/escaped:
619+
# `compgen` which requires its arguments quoted/escaped:
628620
#
629621
# $ ls "a'b/"
630622
# c
@@ -635,28 +627,28 @@ __ltrim_colon_completions()
635627
# See also:
636628
# - https://lists.gnu.org/archive/html/bug-bash/2009-03/msg00155.html
637629
# - https://www.mail-archive.com/[email protected]/msg01944.html
638-
# @param $1 Argument to quote
639-
# @param $2 Name of variable to return result to
640-
_quote_readline_by_ref()
630+
# @param $1 Argument to quote
631+
# @var[out] ret Quoted result is stored in this variable
632+
# shellcheck disable=SC2178 # The assignment is not intended for the global "ret"
633+
_comp_quote_compgen()
641634
{
642635
if [[ $1 == \'* ]]; then
643636
# Leave out first character
644-
printf -v "$2" %s "${1:1}"
637+
ret=${1:1}
645638
else
646-
printf -v "$2" %q "$1"
639+
printf -v ret %q "$1"
647640

648641
# If result becomes quoted like this: $'string', re-evaluate in order
649642
# to drop the additional quoting. See also:
650643
# https://www.mail-archive.com/[email protected]/msg01942.html
651-
if [[ ${!2} == \$\'*\' ]]; then
652-
local value=${!2:2:-1} # Strip beginning $' and ending '.
653-
value=${value//'%'/%%} # Escape % for printf format.
644+
if [[ $ret == \$\'*\' ]]; then
645+
local value=${ret:2:-1} # Strip beginning $' and ending '.
646+
value=${value//'%'/%%} # Escape % for printf format.
654647
# shellcheck disable=SC2059
655-
printf -v value "$value" # Decode escape sequences of \....
656-
local "$2" && _comp_upvars -v "$2" "$value"
648+
printf -v ret "$value" # Decode escape sequences of \....
657649
fi
658650
fi
659-
} # _quote_readline_by_ref()
651+
} # _comp_quote_compgen()
660652

661653
# This function performs file and directory completion. It's better than
662654
# simply using 'compgen -f', because it honours spaces in filenames.
@@ -681,8 +673,9 @@ _filedir()
681673
$reset
682674
IFS=$'\n'
683675
else
684-
local quoted
685-
_quote_readline_by_ref "${cur-}" quoted
676+
local ret
677+
_comp_quote_compgen "${cur-}"
678+
local quoted=$ret
686679

687680
# Munge xspec to contain uppercase version too
688681
# https://lists.gnu.org/archive/html/bug-bash/2010-09/msg00036.html
@@ -2359,11 +2352,15 @@ _filedir_xspec()
23592352
23602353
_tilde "$cur" || return
23612354
2355+
local ret
2356+
_comp_quote_compgen "$cur"
2357+
local quoted=$ret
2358+
23622359
local IFS=$'\n' xspec=${_xspecs[${1##*/}]} tmp
23632360
local -a toks
23642361
23652362
toks=($(
2366-
compgen -d -- "$(quote_readline "$cur")" | {
2363+
compgen -d -- "$quoted" | {
23672364
while read -r tmp; do
23682365
printf '%s\n' "$tmp"
23692366
done
@@ -2382,7 +2379,7 @@ _filedir_xspec()
23822379
xspec="$matchop($xspec|${xspec^^})"
23832380
23842381
toks+=($(
2385-
eval compgen -f -X "'!$xspec'" -- '$(quote_readline "$cur")' | {
2382+
eval compgen -f -X "'!$xspec'" -- '$quoted' | {
23862383
while read -r tmp; do
23872384
[[ $tmp ]] && printf '%s\n' "$tmp"
23882385
done
@@ -2394,7 +2391,7 @@ _filedir_xspec()
23942391
${#toks[@]} -lt 1 ]] && {
23952392
local reset=$(shopt -po noglob)
23962393
set -o noglob
2397-
toks+=($(compgen -f -- "$(quote_readline "$cur")"))
2394+
toks+=($(compgen -f -- "$quoted"))
23982395
IFS=' '
23992396
$reset
24002397
IFS=$'\n'

bash_completion.d/000_bash_completion_compat.bash

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,27 @@ quote()
3434
printf "'%s'" "$quoted"
3535
}
3636

37+
# @deprecated Use `_comp_quote_compgen`
38+
quote_readline()
39+
{
40+
local ret
41+
_comp_quote_compgen "$1"
42+
printf %s "$ret"
43+
} # quote_readline()
44+
45+
# This function is the same as `_comp_quote_compgen`, but receives the second
46+
# argument specifying the variable name to store the result.
47+
# @param $1 Argument to quote
48+
# @param $2 Name of variable to return result to
49+
# @deprecated Use `_comp_quote_compgen "$1"` instead. Note that
50+
# `_comp_quote_compgen` stores the result in a fixed variable `ret`.
51+
_quote_readline_by_ref()
52+
{
53+
[[ $2 == ret ]] || local ret
54+
_comp_quote_compgen "$1"
55+
[[ $2 == ret ]] || printf -v "$2" %s "$ret"
56+
}
57+
3758
# This function shell-dequotes the argument
3859
# @deprecated Use `_comp_dequote' instead. Note that `_comp_dequote` stores
3960
# the results in the array `ret` instead of writing them to stdout.

test/t/unit/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ EXTRA_DIST = \
2121
test_unit_pids.py \
2222
test_unit_pnames.py \
2323
test_unit_quote.py \
24-
test_unit_quote_readline.py \
24+
test_unit_quote_compgen.py \
2525
test_unit_split.py \
2626
test_unit_tilde.py \
2727
test_unit_unlocal.py \
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import os
2+
3+
import pytest
4+
5+
from conftest import assert_bash_exec, assert_complete, bash_env_saved
6+
7+
8+
@pytest.mark.bashcomp(cmd=None, temp_cwd=True)
9+
class TestUnitQuoteCompgen:
10+
@pytest.fixture(scope="class")
11+
def functions(self, bash):
12+
assert_bash_exec(
13+
bash,
14+
'_comp__test_quote_compgen() { local ret; _comp_quote_compgen "$1"; printf %s "$ret"; }',
15+
)
16+
17+
@pytest.mark.parametrize(
18+
"funcname", "_comp__test_quote_compgen quote_readline".split()
19+
)
20+
def test_exec(self, bash, functions, funcname):
21+
assert_bash_exec(bash, "%s '' >/dev/null" % funcname)
22+
23+
@pytest.mark.parametrize(
24+
"funcname", "_comp__test_quote_compgen quote_readline".split()
25+
)
26+
def test_env_non_pollution(self, bash, functions, funcname):
27+
"""Test environment non-pollution, detected at teardown."""
28+
assert_bash_exec(
29+
bash, "foo() { %s meh >/dev/null; }; foo; unset -f foo" % funcname
30+
)
31+
32+
@pytest.mark.parametrize(
33+
"funcname", "_comp__test_quote_compgen quote_readline".split()
34+
)
35+
def test_1(self, bash, functions, funcname):
36+
output = assert_bash_exec(
37+
bash, "%s '';echo" % funcname, want_output=True
38+
)
39+
assert output.strip() == "''"
40+
41+
@pytest.mark.parametrize(
42+
"funcname", "_comp__test_quote_compgen quote_readline".split()
43+
)
44+
def test_2(self, bash, functions, funcname):
45+
output = assert_bash_exec(
46+
bash, "%s foo;echo" % funcname, want_output=True
47+
)
48+
assert output.strip() == "foo"
49+
50+
@pytest.mark.parametrize(
51+
"funcname", "_comp__test_quote_compgen quote_readline".split()
52+
)
53+
def test_3(self, bash, functions, funcname):
54+
output = assert_bash_exec(
55+
bash, '%s foo\\"bar;echo' % funcname, want_output=True
56+
)
57+
assert output.strip() == 'foo\\"bar'
58+
59+
@pytest.mark.parametrize(
60+
"funcname", "_comp__test_quote_compgen quote_readline".split()
61+
)
62+
def test_4(self, bash, functions, funcname):
63+
output = assert_bash_exec(
64+
bash, "%s '$(echo x >&2)';echo" % funcname, want_output=True
65+
)
66+
assert output.strip() == "\\$\\(echo\\ x\\ \\>\\&2\\)"
67+
68+
def test_github_issue_189_1(self, bash, functions):
69+
"""Test error messages on a certain command line
70+
71+
Reported at https://github.com/scop/bash-completion/issues/189
72+
73+
Syntax error messages should not be shown by completion on the
74+
following line:
75+
76+
$ ls -- '${[TAB]
77+
$ rm -- '${[TAB]
78+
79+
"""
80+
assert_bash_exec(bash, "_comp__test_quote_compgen $'\\'${' >/dev/null")
81+
82+
def test_github_issue_492_1(self, bash, functions):
83+
"""Test unintended code execution on a certain command line
84+
85+
Reported at https://github.com/scop/bash-completion/pull/492
86+
87+
Arbitrary commands could be unintendedly executed by
88+
_comp_quote_compgen. In the following example, the command "touch
89+
1.txt" would be unintendedly created before the fix. The file "1.txt"
90+
should not be created by completion on the following line:
91+
92+
$ echo '$(touch file.txt)[TAB]
93+
94+
"""
95+
assert_bash_exec(
96+
bash, "_comp__test_quote_compgen $'\\'$(touch 1.txt)' >/dev/null"
97+
)
98+
assert not os.path.exists("./1.txt")
99+
100+
def test_github_issue_492_2(self, bash, functions):
101+
"""Test the file clear by unintended redirection on a certain command line
102+
103+
Reported at https://github.com/scop/bash-completion/pull/492
104+
105+
The file "1.0" should not be created by completion on the following
106+
line:
107+
108+
$ awk '$1 > 1.0[TAB]
109+
110+
"""
111+
assert_bash_exec(
112+
bash, "_comp__test_quote_compgen $'\\'$1 > 1.0' >/dev/null"
113+
)
114+
assert not os.path.exists("./1.0")
115+
116+
def test_github_issue_492_3(self, bash, functions):
117+
"""Test code execution through unintended pathname expansions
118+
119+
When there is a file named "quote=$(COMMAND)" (for _filedir) or
120+
"ret=$(COMMAND)" (for _comp_quote_compgen), the completion of the word
121+
'$* results in the execution of COMMAND.
122+
123+
$ echo '$*[TAB]
124+
125+
"""
126+
os.mkdir("./ret=$(echo injected >&2)")
127+
assert_bash_exec(bash, "_comp__test_quote_compgen $'\\'$*' >/dev/null")
128+
129+
def test_github_issue_492_4(self, bash, functions):
130+
"""Test error messages through unintended pathname expansions
131+
132+
When "shopt -s failglob" is set by the user, the completion of the word
133+
containing glob character and special characters (e.g. TAB) results in
134+
the failure of pathname expansions.
135+
136+
$ shopt -s failglob
137+
$ echo a\\ b*[TAB]
138+
139+
"""
140+
with bash_env_saved(bash) as bash_env:
141+
bash_env.shopt("failglob", True)
142+
assert_bash_exec(
143+
bash, "_comp__test_quote_compgen $'a\\\\\\tb*' >/dev/null"
144+
)
145+
146+
def test_github_issue_526_1(self, bash):
147+
r"""Regression tests for unprocessed escape sequences after quotes
148+
149+
Ref [1] https://github.com/scop/bash-completion/pull/492#discussion_r637213822
150+
Ref [2] https://github.com/scop/bash-completion/pull/526
151+
152+
The escape sequences in the local variable of "value" in
153+
"_comp_quote_compgen" needs to be unescaped by passing it to printf as
154+
the format string. This causes a problem in the following case [where
155+
the spaces after "alpha\" is a TAB character inserted in the command
156+
string by "C-v TAB"]:
157+
158+
$ echo alpha\ b[TAB]
159+
160+
"""
161+
os.mkdir("./alpha\tbeta")
162+
assert (
163+
assert_complete(
164+
# Remark on "rendered_cmd": Bash aligns the last character 'b'
165+
# in the rendered cmd to an "8 x n" boundary using spaces.
166+
# Here, the command string is assumed to start from column 2
167+
# because the width of PS1 (conftest.PS1 = '/@') is 2,
168+
bash,
169+
"echo alpha\\\026\tb",
170+
rendered_cmd="echo alpha\\ b",
171+
)
172+
== "eta/"
173+
)

0 commit comments

Comments
 (0)