Skip to content

Commit da0df56

Browse files
akinomyogayedayak
andcommitted
fix(java,rsync,ssh): complete syntactically incomplete cur
#1255 (comment) `cur` is the word that is currently input by the user and can be syntactically incomplete when the completion is requested. For example, it is typical to attempt a completion with an opening quotation `'` but without a closing quotation: $ javadoc 'a[tab] Or, if there are two candidates `a\ b.txt` and `a\\\ c.txt`, the first attempt of the completion would insert the common part `a\`, where the escape target of `\` is missing. To handle these cases, in dequoting values coming from `cur`, we need to deal with the cases with incomplete values. This patch adds a new utility `_comp_dequote_incomplete` and replace `_comp_dequote` with it everywhere `_comp_dequote` is used for values coming from `cur`. Co-authored-by: Yedaya Katsman <[email protected]>
1 parent 31675a3 commit da0df56

File tree

9 files changed

+142
-5
lines changed

9 files changed

+142
-5
lines changed

bash_completion

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ _comp_dequote__initialize
208208
# - Quotes \?, '...', "...", $'...', and $"...". In the double
209209
# quotations, parameter expansions are allowed.
210210
#
211+
# Note: To dequote values coming from `cur`, please use the
212+
# function `_comp_dequote_incomplete` instead because `cur` is
213+
# the word that is currently input and can be syntactically
214+
# incomplete.
215+
#
211216
# @var[out] REPLY Array that contains the expanded results. Multiple words or
212217
# no words may be generated through pathname expansions. If
213218
# $1 is not a safe word, REPLY contains the literal value of
@@ -252,6 +257,31 @@ _comp_dequote()
252257
fi
253258
}
254259

260+
# Try to reconstruct an incomplete word and apply _comp_dequote. In
261+
# particular, this function can be used to dequote `cur`, which is currently
262+
# input and can be syntactically incomplete.
263+
#
264+
# @param $1 String to be expanded. The same as _comp_dequote, but
265+
# incomplete backslash, single quotation, and double quotation
266+
# are allowed.
267+
# @var[out] REPLY Result. The same as _comp_dequote.
268+
# @since 2.17
269+
_comp_dequote_incomplete()
270+
{
271+
local _word=${1-}
272+
if ! [[ $_word =~ $_comp_dequote__regex_safe_word ]]; then
273+
# shellcheck disable=SC1003
274+
if [[ ${_word%'\'} =~ $_comp_dequote__regex_safe_word ]]; then
275+
_word=${_word%'\'}
276+
elif [[ $_word\' =~ $_comp_dequote__regex_safe_word ]]; then
277+
_word=$_word\'
278+
elif [[ $_word\" =~ $_comp_dequote__regex_safe_word ]]; then
279+
_word=$_word\"
280+
fi
281+
fi
282+
_comp_dequote "$_word"
283+
}
284+
255285
# Unset the given variables across a scope boundary. Useful for unshadowing
256286
# global scoped variables. Note that simply calling unset on a local variable
257287
# will not unshadow the global variable. Rather, the result will be a local

completions/java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ _comp_cmd_java__packages()
114114
local -a sourcepaths=("${REPLY[@]}")
115115

116116
local REPLY
117-
_comp_dequote "$cur"
117+
_comp_dequote_incomplete "$cur"
118118
local cur_val=${REPLY-}
119119

120120
# convert package syntax to path syntax

completions/ssh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ _comp_xfunc_scp_compgen_remote_files()
543543
# unescape (3 backslashes to 1 for chars we escaped)
544544
REPLY=$(command sed -e 's/\\\\\\\('"$_comp_cmd_scp__path_esc"'\)/\\\1/g' <<<"$REPLY")
545545
fi
546-
_comp_dequote "$REPLY"
546+
_comp_dequote_incomplete "$REPLY"
547547
local cur_val=${REPLY-}
548548

549549
local _userhost=${cur_val%%:*}
@@ -587,7 +587,7 @@ _comp_xfunc_scp_compgen_local_files()
587587
fi
588588

589589
local REPLY
590-
_comp_dequote "$cur"
590+
_comp_dequote_incomplete "$cur"
591591
local cur_val=${REPLY-}
592592

593593
local files

test/fixtures/scp/local_path/backslash-a b.txt

Whitespace-only changes.

test/fixtures/scp/local_path/backslash-a\ b.txt

Whitespace-only changes.

test/t/test_javadoc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ def test_2(self, completion):
1515
)
1616
def test_3(self, completion):
1717
assert completion == ["bar bar.d/", "foo.d/"]
18+
19+
@pytest.mark.complete("javadoc 'shared.d")
20+
def test_4_comp_dequote_incomplete(self, completion):
21+
assert completion == "efault'"

test/t/test_scp.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_basic(self, hosts, completion):
3333
)
3434
),
3535
# Local filenames
36-
["config", "known_hosts", r"spaced\ \ conf"],
36+
["config", "known_hosts", "local_path/", r"spaced\ \ conf"],
3737
)
3838
)
3939
assert completion == expected
@@ -53,7 +53,7 @@ def test_basic_spaced_conf(self, hosts, completion):
5353
)
5454
),
5555
# Local filenames
56-
["config", "known_hosts", r"spaced\ \ conf"],
56+
["config", "known_hosts", "local_path/", r"spaced\ \ conf"],
5757
)
5858
)
5959
assert completion == expected
@@ -120,6 +120,19 @@ def test_remote_path_with_spaces(self, bash):
120120
assert_bash_exec(bash, "unset -f ssh")
121121
assert completion == r"\\\ in\\\ filename.txt"
122122

123+
def test_remote_path_with_backslash(self, bash):
124+
assert_bash_exec(
125+
bash, r"ssh() { printf '%s\n' 'abc def.txt' 'abc\ def.txt'; }"
126+
)
127+
completion = assert_complete(bash, "scp remote_host:abc\\")
128+
assert_bash_exec(bash, "unset -f ssh")
129+
130+
# Note: The number of backslash escaping differs depending on the scp
131+
# version.
132+
assert completion == sorted(
133+
[r"abc\ def.txt", r"abc\\\ def.txt"]
134+
) or completion == sorted([r"abc\\\ def.txt", r"abc\\\\\\\ def.txt"])
135+
123136
def test_xfunc_remote_files(self, live_pwd, bash):
124137
def prefix_paths(prefix, paths):
125138
return [f"{prefix}{path}" for path in paths]
@@ -233,3 +246,9 @@ def test_local_path_with_spaces_1(self, completion):
233246
@pytest.mark.complete(r"scp spaced\ ", cwd="scp")
234247
def test_local_path_with_spaces_2(self, completion):
235248
assert completion == r"\ conf"
249+
250+
@pytest.mark.complete("scp backslash-a\\", cwd="scp/local_path")
251+
def test_local_path_backslash(self, completion):
252+
assert completion == sorted(
253+
[r"backslash-a\ b.txt", r"backslash-a\\\ b.txt"]
254+
)

test/t/unit/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ EXTRA_DIST = \
1919
test_unit_delimited.py \
2020
test_unit_deprecate_func.py \
2121
test_unit_dequote.py \
22+
test_unit_dequote_incomplete.py \
2223
test_unit_expand.py \
2324
test_unit_expand_glob.py \
2425
test_unit_expand_tilde.py \
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
3+
from conftest import assert_bash_exec
4+
5+
6+
@pytest.mark.bashcomp(
7+
cmd=None,
8+
cwd="_filedir",
9+
ignore_env=r"^\+declare -f __tester$",
10+
)
11+
class TestDequoteIncomplete:
12+
@pytest.fixture
13+
def functions(self, bash):
14+
assert_bash_exec(
15+
bash,
16+
'__tester() { local REPLY=dummy v=var;_comp_dequote_incomplete "$1";local ext=$?;((${#REPLY[@]}))&&printf \'<%s>\' "${REPLY[@]}";echo;return $ext;}',
17+
)
18+
19+
def test_basic_1(self, bash, functions):
20+
output = assert_bash_exec(bash, "__tester a", want_output=True)
21+
assert output.strip() == "<a>"
22+
23+
def test_basic_2(self, bash, functions):
24+
output = assert_bash_exec(bash, "__tester abc", want_output=True)
25+
assert output.strip() == "<abc>"
26+
27+
def test_basic_3_null(self, bash, functions):
28+
output = assert_bash_exec(bash, "! __tester ''", want_output=True)
29+
assert output.strip() == ""
30+
31+
def test_basic_4_empty(self, bash, functions):
32+
output = assert_bash_exec(bash, "__tester \"''\"", want_output=True)
33+
assert output.strip() == "<>"
34+
35+
def test_basic_5_brace(self, bash, functions):
36+
output = assert_bash_exec(bash, "__tester 'a{1..3}'", want_output=True)
37+
assert output.strip() == "<a1><a2><a3>"
38+
39+
def test_basic_6_glob(self, bash, functions):
40+
output = assert_bash_exec(bash, "__tester 'a?b'", want_output=True)
41+
assert output.strip() == "<a b><a$b><a&b><a'b>"
42+
43+
def test_quote_1(self, bash, functions):
44+
output = assert_bash_exec(
45+
bash, "__tester '\"a\"'\\'b\\'\\$\\'c\\'", want_output=True
46+
)
47+
assert output.strip() == "<abc>"
48+
49+
def test_quote_2(self, bash, functions):
50+
output = assert_bash_exec(
51+
bash, "__tester '\\\"\\'\\''\\$\\`'", want_output=True
52+
)
53+
assert output.strip() == "<\"'$`>"
54+
55+
def test_quote_3(self, bash, functions):
56+
output = assert_bash_exec(
57+
bash, "__tester \\$\\'a\\\\tb\\'", want_output=True
58+
)
59+
assert output.strip() == "<a\tb>"
60+
61+
def test_quote_4(self, bash, functions):
62+
output = assert_bash_exec(
63+
bash, '__tester \'"abc\\"def"\'', want_output=True
64+
)
65+
assert output.strip() == '<abc"def>'
66+
67+
def test_quote_5(self, bash, functions):
68+
output = assert_bash_exec(
69+
bash, "__tester \\'abc\\'\\\\\\'\\'def\\'", want_output=True
70+
)
71+
assert output.strip() == "<abc'def>"
72+
73+
def test_incomplete_1(self, bash, functions):
74+
output = assert_bash_exec(bash, "__tester 'a\\'", want_output=True)
75+
assert output.strip() == "<a>"
76+
77+
def test_incomplete_2(self, bash, functions):
78+
output = assert_bash_exec(bash, '__tester "\'a b "', want_output=True)
79+
assert output.strip() == "<a b >"
80+
81+
def test_incomplete_3(self, bash, functions):
82+
output = assert_bash_exec(bash, "__tester '\"a b '", want_output=True)
83+
assert output.strip() == "<a b >"

0 commit comments

Comments
 (0)