Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions bash_completion
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ _comp_dequote__initialize
# - Quotes \?, '...', "...", $'...', and $"...". In the double
# quotations, parameter expansions are allowed.
#
# Note: To dequote values coming from `cur`, please use the
# function `_comp_dequote_incomplete` instead because `cur` is
# the word that is currently input and can be syntactically
# incomplete.
#
# @var[out] REPLY Array that contains the expanded results. Multiple words or
# no words may be generated through pathname expansions. If
# $1 is not a safe word, REPLY contains the literal value of
Expand Down Expand Up @@ -252,6 +257,31 @@ _comp_dequote()
fi
}

# Try to reconstruct an incomplete word and apply _comp_dequote. In
# particular, this function can be used to dequote `cur`, which is currently
# input and can be syntactically incomplete.
#
# @param $1 String to be expanded. The same as _comp_dequote, but
# incomplete backslash, single quotation, and double quotation
# are allowed.
# @var[out] REPLY Result. The same as _comp_dequote.
# @since 2.17
_comp_dequote_incomplete()
{
local _word=${1-}
if ! [[ $_word =~ $_comp_dequote__regex_safe_word ]]; then
# shellcheck disable=SC1003
if [[ ${_word%'\'} =~ $_comp_dequote__regex_safe_word ]]; then
_word=${_word%'\'}
elif [[ $_word\' =~ $_comp_dequote__regex_safe_word ]]; then
_word=$_word\'
elif [[ $_word\" =~ $_comp_dequote__regex_safe_word ]]; then
_word=$_word\"
fi
fi
_comp_dequote "$_word"
}

# Unset the given variables across a scope boundary. Useful for unshadowing
# global scoped variables. Note that simply calling unset on a local variable
# will not unshadow the global variable. Rather, the result will be a local
Expand Down
2 changes: 1 addition & 1 deletion completions/java
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ _comp_cmd_java__packages()
local -a sourcepaths=("${REPLY[@]}")

local REPLY
_comp_dequote "$cur"
_comp_dequote_incomplete "$cur"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not immediately clear to me why we'd do this when completing java packages, the commit messages nor the referenced issues don't mention it either, and there's seemingly no test suite coverage for it. Is it necessary, and if yes, can we somehow clarify why and add test cases?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With javadoc 'a[tab], cur becomes an incomplete word cur="'a". If we use _comp_dequote, nothing is completed because modules starting with 'a are not found. In dealing with a value coming from cur, we need to use _comp_dequote_incomplete in general.

  • I rebased on top of the latest main, and I added a test case in test/t/test_javadoc.py.
  • I described this in the commit message of the first commit.
  • I mentioned it in the code documentation of _comp_dequote and _comp_dequote_incomplete.

local cur_val=${REPLY-}

# convert package syntax to path syntax
Expand Down
4 changes: 2 additions & 2 deletions completions/ssh
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ _comp_xfunc_scp_compgen_remote_files()
# unescape (3 backslashes to 1 for chars we escaped)
REPLY=$(command sed -e 's/\\\\\\\('"$_comp_cmd_scp__path_esc"'\)/\\\1/g' <<<"$REPLY")
fi
_comp_dequote "$REPLY"
_comp_dequote_incomplete "$REPLY"
local cur_val=${REPLY-}

local _userhost=${cur_val%%:*}
Expand Down Expand Up @@ -587,7 +587,7 @@ _comp_xfunc_scp_compgen_local_files()
fi

local REPLY
_comp_dequote "$cur"
_comp_dequote_incomplete "$cur"
local cur_val=${REPLY-}

local files
Expand Down
Empty file.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed in the review and already merged, but I think this is a filename that will cause problems e.g. on Windows, and it would be better created dynamically.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty file.
4 changes: 4 additions & 0 deletions test/t/test_javadoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ def test_2(self, completion):
)
def test_3(self, completion):
assert completion == ["bar bar.d/", "foo.d/"]

@pytest.mark.complete("javadoc 'shared.d")
def test_4_comp_dequote_incomplete(self, completion):
assert completion == "efault'"
23 changes: 21 additions & 2 deletions test/t/test_scp.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_basic(self, hosts, completion):
)
),
# Local filenames
["config", "known_hosts", r"spaced\ \ conf"],
["config", "known_hosts", "local_path/", r"spaced\ \ conf"],
)
)
assert completion == expected
Expand All @@ -53,7 +53,7 @@ def test_basic_spaced_conf(self, hosts, completion):
)
),
# Local filenames
["config", "known_hosts", r"spaced\ \ conf"],
["config", "known_hosts", "local_path/", r"spaced\ \ conf"],
)
)
assert completion == expected
Expand Down Expand Up @@ -120,6 +120,19 @@ def test_remote_path_with_spaces(self, bash):
assert_bash_exec(bash, "unset -f ssh")
assert completion == r"\\\ in\\\ filename.txt"

def test_remote_path_with_backslash(self, bash):
assert_bash_exec(
bash, r"ssh() { printf '%s\n' 'abc def.txt' 'abc\ def.txt'; }"
)
completion = assert_complete(bash, "scp remote_host:abc\\")
assert_bash_exec(bash, "unset -f ssh")

# Note: The number of backslash escaping differs depending on the scp
# version.
assert completion == sorted(
[r"abc\ def.txt", r"abc\\\ def.txt"]
) or completion == sorted([r"abc\\\ def.txt", r"abc\\\\\\\ def.txt"])

def test_xfunc_remote_files(self, live_pwd, bash):
def prefix_paths(prefix, paths):
return [f"{prefix}{path}" for path in paths]
Expand Down Expand Up @@ -233,3 +246,9 @@ def test_local_path_with_spaces_1(self, completion):
@pytest.mark.complete(r"scp spaced\ ", cwd="scp")
def test_local_path_with_spaces_2(self, completion):
assert completion == r"\ conf"

@pytest.mark.complete("scp backslash-a\\", cwd="scp/local_path")
def test_local_path_backslash(self, completion):
assert completion == sorted(
[r"backslash-a\ b.txt", r"backslash-a\\\ b.txt"]
)
1 change: 1 addition & 0 deletions test/t/unit/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ EXTRA_DIST = \
test_unit_delimited.py \
test_unit_deprecate_func.py \
test_unit_dequote.py \
test_unit_dequote_incomplete.py \
test_unit_expand.py \
test_unit_expand_glob.py \
test_unit_expand_tilde.py \
Expand Down
4 changes: 2 additions & 2 deletions test/t/unit/test_unit_dequote.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_7_quote_2(self, bash, functions):

def test_7_quote_3(self, bash, functions):
output = assert_bash_exec(
bash, "__tester \\$\\'a\\\\tb\\'", want_output=True
bash, r"__tester \$\'a\\tb\'", want_output=True
)
assert output.strip() == "<a\tb>"

Expand All @@ -66,7 +66,7 @@ def test_7_quote_4(self, bash, functions):

def test_7_quote_5(self, bash, functions):
output = assert_bash_exec(
bash, "__tester \\'abc\\'\\\\\\'\\'def\\'", want_output=True
bash, r"__tester \'abc\'\\\'\'def\'", want_output=True
)
assert output.strip() == "<abc'def>"

Expand Down
83 changes: 83 additions & 0 deletions test/t/unit/test_unit_dequote_incomplete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import pytest

from conftest import assert_bash_exec


@pytest.mark.bashcomp(
cmd=None,
cwd="_filedir",
ignore_env=r"^\+declare -f __tester$",
)
class TestDequoteIncomplete:
@pytest.fixture
def functions(self, bash):
assert_bash_exec(
bash,
'__tester() { local REPLY=dummy v=var;_comp_dequote_incomplete "$1";local ext=$?;((${#REPLY[@]}))&&printf \'<%s>\' "${REPLY[@]}";echo;return $ext;}',
)

def test_basic_1(self, bash, functions):
output = assert_bash_exec(bash, "__tester a", want_output=True)
assert output.strip() == "<a>"

def test_basic_2(self, bash, functions):
output = assert_bash_exec(bash, "__tester abc", want_output=True)
assert output.strip() == "<abc>"

def test_basic_3_null(self, bash, functions):
output = assert_bash_exec(bash, "! __tester ''", want_output=True)
assert output.strip() == ""

def test_basic_4_empty(self, bash, functions):
output = assert_bash_exec(bash, "__tester \"''\"", want_output=True)
assert output.strip() == "<>"

def test_basic_5_brace(self, bash, functions):
output = assert_bash_exec(bash, "__tester 'a{1..3}'", want_output=True)
assert output.strip() == "<a1><a2><a3>"

def test_basic_6_glob(self, bash, functions):
output = assert_bash_exec(bash, "__tester 'a?b'", want_output=True)
assert output.strip() == "<a b><a$b><a&b><a'b>"

def test_quote_1(self, bash, functions):
output = assert_bash_exec(
bash, "__tester '\"a\"'\\'b\\'\\$\\'c\\'", want_output=True
)
assert output.strip() == "<abc>"

def test_quote_2(self, bash, functions):
output = assert_bash_exec(
bash, "__tester '\\\"\\'\\''\\$\\`'", want_output=True
)
assert output.strip() == "<\"'$`>"

def test_quote_3(self, bash, functions):
output = assert_bash_exec(
bash, r"__tester \$\'a\\tb\'", want_output=True
)
assert output.strip() == "<a\tb>"

def test_quote_4(self, bash, functions):
output = assert_bash_exec(
bash, '__tester \'"abc\\"def"\'', want_output=True
)
assert output.strip() == '<abc"def>'

def test_quote_5(self, bash, functions):
output = assert_bash_exec(
bash, r"__tester \'abc\'\\\'\'def\'", want_output=True
)
assert output.strip() == "<abc'def>"

def test_incomplete_1(self, bash, functions):
output = assert_bash_exec(bash, r"__tester 'a\'", want_output=True)
assert output.strip() == "<a>"

def test_incomplete_2(self, bash, functions):
output = assert_bash_exec(bash, '__tester "\'a b "', want_output=True)
assert output.strip() == "<a b >"

def test_incomplete_3(self, bash, functions):
output = assert_bash_exec(bash, "__tester '\"a b '", want_output=True)
assert output.strip() == "<a b >"