diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:03:19 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:03:19 +0000 |
commit | 6c09f2a45c5541e9c207d14fc7aa21a4a0066bde (patch) | |
tree | 0221189d367bf661f6f9493c4f17a03f0dd4b7d2 /test/t/unit | |
parent | Releasing progress-linux version 1:2.11-8~progress7.99u1. (diff) | |
download | bash-completion-6c09f2a45c5541e9c207d14fc7aa21a4a0066bde.tar.xz bash-completion-6c09f2a45c5541e9c207d14fc7aa21a4a0066bde.zip |
Merging upstream version 1:2.12.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/t/unit')
39 files changed, 2063 insertions, 201 deletions
diff --git a/test/t/unit/Makefile.am b/test/t/unit/Makefile.am index 3eb652a..54722de 100644 --- a/test/t/unit/Makefile.am +++ b/test/t/unit/Makefile.am @@ -1,21 +1,38 @@ EXTRA_DIST = \ + test_unit_abspath.py \ + test_unit_command_offset.py \ + test_unit_compgen.py \ + test_unit_compgen_commands.py \ test_unit_count_args.py \ + test_unit_delimited.py \ + test_unit_deprecate_func.py \ + test_unit_dequote.py \ test_unit_expand.py \ - test_unit_expand_tilde_by_ref.py \ + test_unit_expand_glob.py \ + test_unit_expand_tilde.py \ test_unit_filedir.py \ test_unit_find_unique_completion_pair.py \ - test_unit_get_comp_words_by_ref.py \ + test_unit_get_first_arg.py \ test_unit_get_cword.py \ - test_unit_init_completion.py \ + test_unit_get_words.py \ + test_unit_initialize.py \ test_unit_ip_addresses.py \ - test_unit_known_hosts_real.py \ + test_unit_known_hosts.py \ test_unit_longopt.py \ + test_unit_looks_like_path.py \ test_unit_parse_help.py \ test_unit_parse_usage.py \ + test_unit_pgids.py \ + test_unit_pids.py \ + test_unit_pnames.py \ test_unit_quote.py \ - test_unit_quote_readline.py \ + test_unit_quote_compgen.py \ + test_unit_realcommand.py \ + test_unit_split.py \ test_unit_tilde.py \ + test_unit_unlocal.py \ test_unit_variables.py \ + test_unit_xfunc.py \ test_unit_xinetd_services.py all: diff --git a/test/t/unit/test_unit_abspath.py b/test/t/unit/test_unit_abspath.py new file mode 100644 index 0000000..97d506c --- /dev/null +++ b/test/t/unit/test_unit_abspath.py @@ -0,0 +1,67 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp( + cmd=None, cwd="shared", ignore_env=r"^\+declare -f __tester$" +) +class TestUnitAbsPath: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + ( + "__tester() { " + "local REPLY; " + '_comp_abspath "$1"; ' + 'printf %s "$REPLY"; ' + "}" + ), + ) + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local REPLY=; _comp_abspath bar; }; foo; unset -f foo", + want_output=None, + ) + + def test_absolute(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/foo/bar" + + def test_relative(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/foo/bar") + + def test_cwd(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester ./foo/./bar", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/foo/bar") + + def test_parent(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester ../shared/foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith( + "/shared/foo/bar" + ) and not output.strip().endswith("../shared/foo/bar") diff --git a/test/t/unit/test_unit_command_offset.py b/test/t/unit/test_unit_command_offset.py new file mode 100644 index 0000000..0e32c1f --- /dev/null +++ b/test/t/unit/test_unit_command_offset.py @@ -0,0 +1,144 @@ +from shlex import quote + +import pytest + +from conftest import assert_bash_exec, assert_complete, bash_env_saved + + +def join(words): + """Return a shell-escaped string from *words*.""" + return " ".join(quote(word) for word in words) + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_command_offset", + ignore_env=r"^[+-](COMPREPLY|REPLY)=", +) +class TestUnitCommandOffset: + wordlist = sorted(["foo", "bar"]) + + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + "_cmd1() { _comp_command_offset 1; }; complete -F _cmd1 cmd1; " + "complete -F _comp_command meta; " + "_compfunc() { COMPREPLY=(%s); }" % join(self.wordlist), + ) + + completions = [ + 'complete -F _compfunc "${COMP_WORDS[0]}"', + 'complete -W %s "${COMP_WORDS[0]}"' % quote(join(self.wordlist)), + 'COMPREPLY=(dummy); complete -r "${COMP_WORDS[0]}"', + "COMPREPLY+=(${#COMPREPLY[@]})", + ] + for idx, comp in enumerate(completions, 2): + assert_bash_exec( + bash, + "_cmd%(idx)s() { %(comp)s && return 124; }; " + "complete -F _cmd%(idx)s cmd%(idx)s" + % {"idx": idx, "comp": comp}, + ) + + assert_bash_exec( + bash, "complete -W %s 'cmd!'" % quote(join(self.wordlist)) + ) + assert_bash_exec(bash, 'complete -W \'"$word1" "$word2"\' cmd6') + + assert_bash_exec(bash, "complete -C ./completer cmd7") + + def test_1(self, bash, functions): + assert_complete(bash, 'cmd1 "/tmp/aaa bbb" ') + assert_bash_exec(bash, "! complete -p aaa", want_output=None) + + @pytest.mark.parametrize( + "cmd,expected_completion", + [ + ("cmd2", wordlist), + ("cmd3", wordlist), + ("cmd4", []), + ("cmd5", ["0"]), + ], + ) + def test_2(self, bash, functions, cmd, expected_completion): + """Test meta-completion for completion functions that signal that + completion should be retried (i.e. change compspec and return 124). + + cmd2: The case when the completion spec is overwritten by the one that + contains "-F func" + + cmd3: The case when the completion spec is overwritten by the one + without "-F func". + + cmd4: The case when the completion spec is removed, in which we expect + no completions. This mimics the behavior of Bash's progcomp for the + exit status 124. + + cmd5: The case when the completion spec is unchanged. The retry should + be attempted at most once to avoid infinite loops. COMPREPLY should be + cleared before the retry. + """ + assert assert_complete(bash, "meta %s " % cmd) == expected_completion + + @pytest.mark.parametrize( + "cmd,expected_completion", + [ + ("cmd7 ", wordlist), + ("cmd7 l", ["line\\^Jtwo", "long"]), + ("cmd7 lo", ["ng"]), + ("cmd7 line", ["\\^Jtwo"]), + ("cmd7 cont1", ["cont10", "cont11\\"]), + ], + ) + def test_3(self, bash, functions, cmd, expected_completion): + got = assert_complete(bash, f"cmd1 {cmd}") + assert got == assert_complete(bash, cmd) + assert got == expected_completion + + def test_cmd_quoted(self, bash, functions): + assert assert_complete(bash, "meta 'cmd2' ") == self.wordlist + + def test_cmd_specialchar(self, bash, functions): + assert assert_complete(bash, "meta 'cmd!' ") == self.wordlist + + def test_space(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("word1", "a b c") + bash_env.write_variable("word2", "d e f") + assert assert_complete(bash, "meta cmd6 ") == ["a b c", "d e f"] + + @pytest.fixture(scope="class") + def find_original_word_functions(self, bash): + assert_bash_exec( + bash, + "_comp_test_reassemble() {" + " local IFS=$' \\t\\n' REPLY;" + ' COMP_LINE=$1; _comp_split COMP_WORDS "$2"; COMP_CWORD=$((${#COMP_WORDS[@]}-1));' + " _comp__reassemble_words = words cword;" + "}", + ) + assert_bash_exec( + bash, + "_comp_test_1() {" + ' local COMP_WORDS COMP_LINE COMP_CWORD words cword REPLY; _comp_test_reassemble "$1" "$2";' + ' _comp__find_original_word "$3";' + ' echo "$REPLY";' + "}", + ) + + def test_find_original_word_1(self, bash, find_original_word_functions): + result = assert_bash_exec( + bash, + '_comp_test_1 "sudo su do su do abc" "sudo su do su do abc" 3', + want_output=True, + ).strip() + assert result == "3" + + def test_find_original_word_2(self, bash, find_original_word_functions): + result = assert_bash_exec( + bash, + '_comp_test_1 "sudo --prefix=su su do abc" "sudo --prefix = su su do abc" 2', + want_output=True, + ).strip() + assert result == "4" diff --git a/test/t/unit/test_unit_compgen.py b/test/t/unit/test_unit_compgen.py new file mode 100644 index 0000000..cfdec8e --- /dev/null +++ b/test/t/unit/test_unit_compgen.py @@ -0,0 +1,173 @@ +import pytest +import re + +from conftest import assert_bash_exec, bash_env_saved, assert_complete + + +@pytest.mark.bashcomp(cmd=None) +class TestUtilCompgen: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + "_comp__test_dump() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }", + ) + assert_bash_exec( + bash, + '_comp__test_compgen() { local -a arr=(00); _comp_compgen -v arr "$@"; _comp__test_dump; }', + ) + assert_bash_exec( + bash, + '_comp__test_words() { local -a input=("${@:1:$#-1}"); _comp__test_compgen -c "${@:$#}" -- -W \'${input[@]+"${input[@]}"}\'; }', + ) + assert_bash_exec( + bash, + '_comp__test_words_ifs() { local input=$2; _comp__test_compgen -F "$1" -c "${@:$#}" -- -W \'$input\'; }', + ) + + assert_bash_exec( + bash, + '_comp_cmd_fc() { _comp_compgen -c "$(_get_cword)" -C _filedir filedir; }; ' + "complete -F _comp_cmd_fc fc; " + "complete -F _comp_cmd_fc -o filenames fc2", + ) + assert_bash_exec( + bash, + '_comp_cmd_fcd() { _comp_compgen -c "$(_get_cword)" -C _filedir filedir -d; }; ' + "complete -F _comp_cmd_fcd fcd", + ) + + # test_8_option_U + assert_bash_exec( + bash, + "_comp_compgen_gen8() { local -a arr=(x y z); _comp_compgen -U arr -- -W '\"${arr[@]}\"'; }", + ) + + # test_9_inherit_a + assert_bash_exec( + bash, + '_comp_compgen_gen9sub() { local -a gen=(00); _comp_compgen -v gen -- -W 11; _comp_compgen_set "${gen[@]}"; }; ' + "_comp_compgen_gen9() { _comp_compgen_gen9sub; _comp_compgen -a gen9sub; }", + ) + + def test_1_basic(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_words 12 34 56 ''", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_2_space(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_words $'a b' $'c d\\t' ' e ' $'\\tf\\t' ''", + want_output=True, + ) + assert output.strip() == "<a b><c d\t>< e ><\tf\t>" + + def test_2_IFS(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("IFS", "34") + output = assert_bash_exec( + bash, "_comp__test_words 12 34 56 ''", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_3_glob(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_words '*' '[a-z]*' '[a][b][c]' ''", + want_output=True, + ) + assert output.strip() == "<*><[a-z]*><[a][b][c]>" + + def test_3_failglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, + "_comp__test_words '*' '[a-z]*' '[a][b][c]' ''", + want_output=True, + ) + assert output.strip() == "<*><[a-z]*><[a][b][c]>" + + def test_3_nullglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, + "_comp__test_words '*' '[a-z]*' '[a][b][c]' ''", + want_output=True, + ) + assert output.strip() == "<*><[a-z]*><[a][b][c]>" + + def test_4_empty(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_words ''", want_output=True + ) + assert output.strip() == "" + + def test_5_option_F(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_words_ifs '25' ' 123 456 555 ' ''", + want_output=True, + ) + assert output.strip() == "< 1><3 4><6 >< >" + + def test_6_option_C_1(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_compgen -c a -C _filedir filedir", + want_output=True, + ) + set1 = set(re.findall(r"<[^<>]*>", output.strip())) + assert set1 == {"<a b>", "<a$b>", "<a&b>", "<a'b>", "<ab>", "<aƩ>"} + + def test_6_option_C_2(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_compgen -c b -C _filedir -- -d", + want_output=True, + ) + assert output.strip() == "<brackets>" + + @pytest.mark.parametrize("funcname", "fc fc2".split()) + def test_6_option_C_3(self, bash, functions, funcname): + completion = assert_complete(bash, "%s _filedir ab/" % funcname) + assert completion == "e" + + @pytest.mark.complete(r"fcd a\ ") + def test_6_option_C_4(self, functions, completion): + # Note: we are not in the original directory that "b" exists, so Bash + # will not suffix a slash to the directory name. + assert completion == "b" + + def test_7_icmd(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_compgen", quote=False + ) + + completions = assert_complete(bash, "compgen-cmd1 '") + assert completions == ["012", "123", "234", "5abc", "6def", "7ghi"] + + def test_7_xcmd(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_compgen", quote=False + ) + + completions = assert_complete(bash, "compgen-cmd2 '") + assert completions == ["012", "123", "234", "5foo", "6bar", "7baz"] + + def test_8_option_U(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_compgen gen8", want_output=True + ) + assert output.strip() == "<x><y><z>" + + def test_9_inherit_a(self, bash, functions): + output = assert_bash_exec( + bash, "_comp__test_compgen gen9", want_output=True + ) + assert output.strip() == "<11><11>" diff --git a/test/t/unit/test_unit_compgen_commands.py b/test/t/unit/test_unit_compgen_commands.py new file mode 100644 index 0000000..d866239 --- /dev/null +++ b/test/t/unit/test_unit_compgen_commands.py @@ -0,0 +1,47 @@ +import pytest + +from conftest import assert_bash_exec, assert_complete, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") +class TestUtilCompgenCommands: + @pytest.fixture(scope="class") + def functions(self, request, bash): + assert_bash_exec( + bash, + r"_comp_compgen_commands__test() {" + r" local COMPREPLY=() cur=${1-};" + r" _comp_compgen_commands;" + r' printf "%s\n" "${COMPREPLY[@]-}";' + r"}", + ) + assert_bash_exec( + bash, + "_comp_cmd_ccc() {" + " local cur;" + " _comp_get_words cur;" + " unset -v COMPREPLY;" + " _comp_compgen_commands;" + "}; complete -F _comp_cmd_ccc ccc", + ) + + def test_basic(self, bash, functions): + output = assert_bash_exec( + bash, "_comp_compgen_commands__test sh", want_output=True + ) + assert output.strip() + + @pytest.mark.parametrize( + "shopt_no_empty,result_empty", ((True, True), (False, False)) + ) + def test_empty(self, bash, functions, shopt_no_empty, result_empty): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("no_empty_cmd_completion", shopt_no_empty) + output = assert_bash_exec( + bash, "_comp_compgen_commands__test", want_output=True + ) + assert (output.strip() == "") == result_empty + + def test_spaces(self, bash, functions): + completion = assert_complete(bash, "ccc shared/default/bar") + assert completion == r"\ bar.d/" diff --git a/test/t/unit/test_unit_compgen_split.py b/test/t/unit/test_unit_compgen_split.py new file mode 100644 index 0000000..935ea2a --- /dev/null +++ b/test/t/unit/test_unit_compgen_split.py @@ -0,0 +1,102 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUtilCompgenSplit: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + "_comp__test_dump() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }", + ) + assert_bash_exec( + bash, + '_comp__test_compgen() { local -a arr=(00); _comp_compgen -v arr "$@"; _comp__test_dump; }', + ) + + assert_bash_exec( + bash, + "_comp__test_cmd1() { echo foo bar; echo baz; }", + ) + assert_bash_exec( + bash, + '_comp__test_attack() { echo "\\$(echo should_not_run >&2)"; }', + ) + + def test_1_basic(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo><bar><baz>" + + def test_2_attack(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -- "$(_comp__test_attack)"', + want_output=True, + ) + assert output.strip() == "<$(echo><should_not_run><>&2)>" + + def test_3_sep1(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -l -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo bar><baz>" + + def test_3_sep2(self, bash, functions): + output = assert_bash_exec( + bash, + "_comp__test_compgen split -F $'b\\n' -- \"$(_comp__test_cmd1)\"", + want_output=True, + ) + assert output.strip() == "<foo ><ar><az>" + + def test_4_optionX(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -X bar -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo><baz>" + + def test_4_optionS(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -S .txt -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<foo.txt><bar.txt><baz.txt>" + + def test_4_optionP(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -P /tmp/ -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "</tmp/foo></tmp/bar></tmp/baz>" + + def test_4_optionPS(self, bash, functions): + output = assert_bash_exec( + bash, + '_comp__test_compgen split -P [ -S ] -- "$(_comp__test_cmd1)"', + want_output=True, + ) + assert output.strip() == "<[foo]><[bar]><[baz]>" + + def test_5_empty(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_compgen split -- ""', want_output=True + ) + assert output.strip() == "" + + def test_5_empty2(self, bash, functions): + output = assert_bash_exec( + bash, '_comp__test_compgen split -- " "', want_output=True + ) + assert output.strip() == "" diff --git a/test/t/unit/test_unit_count_args.py b/test/t/unit/test_unit_count_args.py index 56bce2c..7b018e4 100644 --- a/test/t/unit/test_unit_count_args.py +++ b/test/t/unit/test_unit_count_args.py @@ -4,63 +4,167 @@ from conftest import TestUnitBase, assert_bash_exec @pytest.mark.bashcomp( - cmd=None, ignore_env=r"^[+-](args|COMP_(WORDS|CWORD|LINE|POINT))=" + cmd=None, + ignore_env=r"^[+-](REPLY|cword|words|COMP_(WORDS|CWORD|LINE|POINT))=", ) class TestUnitCountArgs(TestUnitBase): + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_unit() { local -a words=(); local cword REPLY=""; _comp__reassemble_words "<>&" words cword; _comp_count_args "$@"; echo "$REPLY"; }', + ) + def _test(self, *args, **kwargs): - return self._test_unit("_count_args %s; echo $args", *args, **kwargs) + return self._test_unit("_comp__test_unit %s", *args, **kwargs) def test_1(self, bash): - assert_bash_exec(bash, "COMP_CWORD= _count_args >/dev/null") + assert_bash_exec( + bash, + 'COMP_LINE= COMP_POINT=0 COMP_WORDS=() COMP_CWORD=; _comp_count_args -n ""', + ) - def test_2(self, bash): + def test_2(self, bash, functions): """a b| should set args to 1""" output = self._test(bash, "(a b)", 1, "a b", 3) assert output == "1" - def test_3(self, bash): + def test_3(self, bash, functions): """a b|c should set args to 1""" output = self._test(bash, "(a bc)", 1, "a bc", 3) assert output == "1" - def test_4(self, bash): + def test_4(self, bash, functions): """a b c| should set args to 2""" output = self._test(bash, "(a b c)", 2, "a b c", 4) assert output == "2" - def test_5(self, bash): + def test_5(self, bash, functions): """a b| c should set args to 1""" output = self._test(bash, "(a b c)", 1, "a b c", 3) assert output == "1" - def test_6(self, bash): + def test_6(self, bash, functions): """a b -c| d should set args to 2""" output = self._test(bash, "(a b -c d)", 2, "a b -c d", 6) assert output == "2" - def test_7(self, bash): + def test_7(self, bash, functions): """a b -c d e| with -c arg excluded should set args to 2""" output = self._test( - bash, "(a b -c d e)", 4, "a b -c d e", 10, arg='"" "@(-c|--foo)"' + bash, "(a b -c d e)", 4, "a b -c d e", 10, arg='-a "@(-c|--foo)"' ) assert output == "2" - def test_8(self, bash): + def test_8(self, bash, functions): """a -b -c d e| with -c arg excluded - and -b included should set args to 1""" + and -b included should set args to 1""" output = self._test( bash, "(a -b -c d e)", 4, "a -b -c d e", 11, - arg='"" "@(-c|--foo)" "-[b]"', + arg='-a "@(-c|--foo)" -i "-[b]"', ) assert output == "2" - def test_9(self, bash): + def test_9(self, bash, functions): """a -b -c d e| with -b included should set args to 3""" output = self._test( - bash, "(a -b -c d e)", 4, "a -b -c d e", 11, arg='"" "" "-b"' + bash, "(a -b -c d e)", 4, "a -b -c d e", 11, arg='-i "-b"' + ) + assert output == "3" + + def test_10_single_hyphen_1(self, bash): + """- should be counted as an argument representing stdout/stdin""" + output = self._test(bash, "(a -b - c -d e)", 5, "a -b - c -d e", 12) + assert output == "3" + + def test_10_single_hyphen_2(self, bash): + """- in an option argument should be skipped""" + output = self._test( + bash, "(a -b - c - e)", 5, "a -b - c - e", 11, arg='-a "-b"' + ) + assert output == "3" + + def test_11_double_hyphen_1(self, bash): + """all the words after -- should be counted""" + output = self._test( + bash, "(a -b -- -c -d e)", 5, "a -b -- -c -d e", 14 ) assert output == "3" + + def test_11_double_hyphen_2(self, bash): + """all the words after -- should be counted""" + output = self._test(bash, "(a b -- -c -d e)", 5, "a b -- -c -d e", 13) + assert output == "4" + + def test_12_exclude_optarg_1(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, "(a -o -x b c)", 4, "a -o -x b c", 10, arg='-a "-o" -i "-x"' + ) + assert output == "2" + + def test_12_exclude_optarg_2(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, + "(a -o -x -x c)", + 4, + "a -o -x -x c", + 11, + arg='-a "-o" -i "-x"', + ) + assert output == "2" + + def test_12_exclude_optarg_3(self, bash): + """an option argument should be skipped even if it matches the argument pattern""" + output = self._test( + bash, + "(a -o -x -y c)", + 4, + "a -o -x -y c", + 11, + arg='-a "-o" -i "-x"', + ) + assert output == "1" + + def test_13_plus_option_optarg(self, bash): + """When +o is specified to be an option taking an option argument, it should not be counted as an argument""" + output = self._test( + bash, "(a +o b c)", 3, "a +o b c", 7, arg='-a "+o"' + ) + assert output == "1" + + def test_14_no_optarg_chain_1(self, bash): + """an option argument should not take another option argument""" + output = self._test( + bash, "(a -o -o -o -o c)", 5, "a -o -o -o -o c", 14, arg='-a "-o"' + ) + assert output == "1" + + def test_14_no_optarg_chain_2(self, bash): + """an option argument should not take another option argument""" + output = self._test( + bash, + "(a -o -o b -o -o c)", + 6, + "a -o -o b -o -o c", + 16, + arg='-a "-o"', + ) + assert output == "2" + + def test_15_double_hyphen_optarg(self, bash): + """-- should lose its meaning when it is an option argument""" + output = self._test( + bash, "(a -o -- -b -c d)", 5, "a -o -- -b -c d", 14, arg='-a "-o"' + ) + assert output == "1" + + def test_16_empty_word(self, bash): + """An empty word should not take an option argument""" + output = self._test(bash, "(a '' x '' y d)", 5, "a x y d", 8) + assert output == "5" diff --git a/test/t/unit/test_unit_delimited.py b/test/t/unit/test_unit_delimited.py new file mode 100644 index 0000000..e20dcd1 --- /dev/null +++ b/test/t/unit/test_unit_delimited.py @@ -0,0 +1,42 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitDelimited: + @pytest.fixture(scope="class") + def functions(self, request, bash): + assert_bash_exec( + bash, + "_comp_cmd_test_delim() {" + " local cur prev words cword comp_args;" + " _comp_get_words cur;" + " _comp_delimited , -W 'alpha beta bravo';" + "};" + "complete -F _comp_cmd_test_delim test_delim", + ) + + @pytest.mark.complete("test_delim --opt=a") + def test_1(self, functions, completion): + assert completion == ["lpha"] + + @pytest.mark.complete("test_delim --opt=b") + def test_2(self, functions, completion): + assert completion == ["beta", "bravo"] + + @pytest.mark.complete("test_delim --opt=alpha,b") + def test_3(self, functions, completion): + assert completion == ["alpha,beta", "alpha,bravo"] + + @pytest.mark.complete("test_delim --opt=alpha,be") + def test_4(self, functions, completion): + assert completion == ["ta"] + + @pytest.mark.complete("test_delim --opt=beta,a") + def test_5(self, functions, completion): + assert completion == ["lpha"] + + @pytest.mark.complete("test_delim --opt=c") + def test_6(self, functions, completion): + assert not completion diff --git a/test/t/unit/test_unit_deprecate_func.py b/test/t/unit/test_unit_deprecate_func.py new file mode 100644 index 0000000..c825e8c --- /dev/null +++ b/test/t/unit/test_unit_deprecate_func.py @@ -0,0 +1,15 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+declare -f func[12]$") +class TestUnitDeprecateFunc: + def test_1(self, bash): + assert_bash_exec( + bash, + 'func1() { echo "func1($*)"; }; ' + "_comp_deprecate_func 2.12 func2 func1", + ) + output = assert_bash_exec(bash, "func2 1 2 3", want_output=True) + assert output.strip() == "func1(1 2 3)" diff --git a/test/t/unit/test_unit_dequote.py b/test/t/unit/test_unit_dequote.py new file mode 100644 index 0000000..0ac814d --- /dev/null +++ b/test/t/unit/test_unit_dequote.py @@ -0,0 +1,161 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_filedir", + ignore_env=r"^\+declare -f __tester$", +) +class TestDequote: + def test_1_char(self, bash): + assert_bash_exec( + bash, + '__tester() { local REPLY=dummy v=var;_comp_dequote "$1";local ext=$?;((${#REPLY[@]}))&&printf \'<%s>\' "${REPLY[@]}";echo;return $ext;}', + ) + output = assert_bash_exec(bash, "__tester a", want_output=True) + assert output.strip() == "<a>" + + def test_2_str(self, bash): + output = assert_bash_exec(bash, "__tester abc", want_output=True) + assert output.strip() == "<abc>" + + def test_3_null(self, bash): + output = assert_bash_exec(bash, "__tester ''", want_output=True) + assert output.strip() == "" + + def test_4_empty(self, bash): + output = assert_bash_exec(bash, "__tester \"''\"", want_output=True) + assert output.strip() == "<>" + + def test_5_brace(self, bash): + output = assert_bash_exec(bash, "__tester 'a{1..3}'", want_output=True) + assert output.strip() == "<a1><a2><a3>" + + def test_6_glob(self, bash): + output = assert_bash_exec(bash, "__tester 'a?b'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b>" + + def test_7_quote_1(self, bash): + output = assert_bash_exec( + bash, "__tester '\"a\"'\\'b\\'\\$\\'c\\'", want_output=True + ) + assert output.strip() == "<abc>" + + def test_7_quote_2(self, bash): + output = assert_bash_exec( + bash, "__tester '\\\"\\'\\''\\$\\`'", want_output=True + ) + assert output.strip() == "<\"'$`>" + + def test_7_quote_3(self, bash): + output = assert_bash_exec( + bash, "__tester \\$\\'a\\\\tb\\'", want_output=True + ) + assert output.strip() == "<a\tb>" + + def test_7_quote_4(self, bash): + output = assert_bash_exec( + bash, '__tester \'"abc\\"def"\'', want_output=True + ) + assert output.strip() == '<abc"def>' + + def test_7_quote_5(self, bash): + output = assert_bash_exec( + bash, "__tester \\'abc\\'\\\\\\'\\'def\\'", want_output=True + ) + assert output.strip() == "<abc'def>" + + def test_8_param_1(self, bash): + output = assert_bash_exec(bash, "__tester '$v'", want_output=True) + assert output.strip() == "<var>" + + def test_8_param_2(self, bash): + output = assert_bash_exec(bash, "__tester '${v}'", want_output=True) + assert output.strip() == "<var>" + + def test_8_param_3(self, bash): + output = assert_bash_exec(bash, "__tester '${#v}'", want_output=True) + assert output.strip() == "<3>" + + def test_8_param_4(self, bash): + output = assert_bash_exec(bash, "__tester '${v[0]}'", want_output=True) + assert output.strip() == "<var>" + + def test_9_qparam_1(self, bash): + output = assert_bash_exec(bash, "__tester '\"$v\"'", want_output=True) + assert output.strip() == "<var>" + + def test_9_qparam_2(self, bash): + output = assert_bash_exec( + bash, "__tester '\"${v[@]}\"'", want_output=True + ) + assert output.strip() == "<var>" + + def test_10_pparam_1(self, bash): + output = assert_bash_exec(bash, "__tester '$?'", want_output=True) + assert output.strip() == "<0>" + + def test_10_pparam_2(self, bash): + output = assert_bash_exec(bash, "__tester '${#1}'", want_output=True) + assert output.strip() == "<5>" # The string `${#1}` is five characters + + def test_unsafe_1(self, bash): + output = assert_bash_exec( + bash, "! __tester '$(echo hello >&2)'", want_output=True + ) + assert output.strip() == "" + + def test_unsafe_2(self, bash): + output = assert_bash_exec( + bash, "! __tester '|echo hello >&2'", want_output=True + ) + assert output.strip() == "" + + def test_unsafe_3(self, bash): + output = assert_bash_exec( + bash, "! __tester '>| important_file.txt'", want_output=True + ) + assert output.strip() == "" + + def test_unsafe_4(self, bash): + output = assert_bash_exec( + bash, "! __tester '`echo hello >&2`'", want_output=True + ) + assert output.strip() == "" + + def test_glob_default(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", False) + bash_env.shopt("nullglob", False) + output = assert_bash_exec( + bash, "__tester 'non-existent-*.txt'", want_output=True + ) + assert output.strip() == "<non-existent-*.txt>" + + def test_glob_noglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.set("noglob", True) + output = assert_bash_exec( + bash, + "__tester 'non-existent-*.txt'", + want_output=True, + ) + assert output.strip() == "<non-existent-*.txt>" + + def test_glob_failglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "! __tester 'non-existent-*.txt'", want_output=True + ) + assert output.strip() == "" + + def test_glob_nullglob(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "__tester 'non-existent-*.txt'", want_output=True + ) + assert output.strip() == "" diff --git a/test/t/unit/test_unit_expand.py b/test/t/unit/test_unit_expand.py index d2a3ebc..0be6c52 100644 --- a/test/t/unit/test_unit_expand.py +++ b/test/t/unit/test_unit_expand.py @@ -1,31 +1,42 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp(cmd=None, ignore_env=r"^[+-](cur|COMPREPLY)=") class TestUnitExpand: def test_1(self, bash): - assert_bash_exec(bash, "_expand >/dev/null") + assert_bash_exec(bash, "_comp_expand >/dev/null") def test_2(self, bash): """Test environment non-pollution, detected at teardown.""" - assert_bash_exec(bash, "foo() { _expand; }; foo; unset foo") + assert_bash_exec(bash, "foo() { _comp_expand; }; foo; unset -f foo") def test_user_home_compreply(self, bash, user_home): user, home = user_home output = assert_bash_exec( bash, - r'cur="~%s"; _expand; printf "%%s\n" "$COMPREPLY"' % user, + r'cur="~%s"; _comp_expand; printf "%%s\n" "$COMPREPLY"' % user, want_output=True, ) assert output.strip() == home + def test_user_home_compreply_failglob(self, bash, user_home): + user, home = user_home + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, + r'cur="~%s"; _comp_expand; printf "%%s\n" "$COMPREPLY"' % user, + want_output=True, + ) + assert output.strip() == home + def test_user_home_cur(self, bash, user_home): user, home = user_home output = assert_bash_exec( bash, - r'cur="~%s/a"; _expand; printf "%%s\n" "$cur"' % user, + r'cur="~%s/a"; _comp_expand; printf "%%s\n" "$cur"' % user, want_output=True, ) assert output.strip() == "%s/a" % home diff --git a/test/t/unit/test_unit_expand_glob.py b/test/t/unit/test_unit_expand_glob.py new file mode 100644 index 0000000..64d04a7 --- /dev/null +++ b/test/t/unit/test_unit_expand_glob.py @@ -0,0 +1,83 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, + cwd="_filedir", + ignore_env=r"^\+declare -f (dump_array|__tester)$", +) +class TestExpandGlob: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + "dump_array() { ((${#arr[@]})) && printf '<%s>' \"${arr[@]}\"; echo; }", + ) + assert_bash_exec( + bash, + '__tester() { local LC_ALL= LC_COLLATE=C arr; _comp_expand_glob arr "$@";dump_array; }', + ) + + def test_match_all(self, bash, functions): + output = assert_bash_exec(bash, "__tester '*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aƩ><brackets><ext>" + + def test_match_pattern(self, bash, functions): + output = assert_bash_exec(bash, "__tester 'a*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aƩ>" + + def test_match_unmatched(self, bash, functions): + output = assert_bash_exec( + bash, "__tester 'unmatched-*'", want_output=True + ) + assert output.strip() == "" + + def test_match_multiple_words(self, bash, functions): + output = assert_bash_exec(bash, "__tester 'b* e*'", want_output=True) + assert output.strip() == "<brackets><ext>" + + def test_match_brace_expansion(self, bash, functions): + output = assert_bash_exec( + bash, "__tester 'brac{ket,unmatched}*'", want_output=True + ) + assert output.strip() == "<brackets>" + + def test_protect_from_noglob(self, bash, functions): + with bash_env_saved(bash, functions) as bash_env: + bash_env.set("noglob", True) + output = assert_bash_exec(bash, "__tester 'a*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aƩ>" + + def test_protect_from_failglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "__tester 'unmatched-*'", want_output=True + ) + assert output.strip() == "" + + def test_protect_from_nullglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", False) + output = assert_bash_exec( + bash, "__tester 'unmatched-*'", want_output=True + ) + assert output.strip() == "" + + def test_protect_from_dotglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("dotglob", True) + output = assert_bash_exec( + bash, "__tester 'ext/foo/*'", want_output=True + ) + assert output.strip() == "" + + def test_protect_from_GLOBIGNORE(self, bash, functions): + with bash_env_saved(bash) as bash_env: + # Note: dotglob is changed by GLOBIGNORE + bash_env.save_shopt("dotglob") + bash_env.write_variable("GLOBIGNORE", "*") + output = assert_bash_exec(bash, "__tester 'a*'", want_output=True) + assert output.strip() == "<a b><a$b><a&b><a'b><ab><aƩ>" diff --git a/test/t/unit/test_unit_expand_tilde_by_ref.py b/test/t/unit/test_unit_expand_tilde.py index 17bdedf..3552fd6 100644 --- a/test/t/unit/test_unit_expand_tilde_by_ref.py +++ b/test/t/unit/test_unit_expand_tilde.py @@ -3,16 +3,27 @@ import pytest from conftest import assert_bash_exec -@pytest.mark.bashcomp(cmd=None, ignore_env=r"^[+-]var=") -class TestUnitExpandTildeByRef: +@pytest.mark.bashcomp(cmd=None) +class TestUnitExpandTilde: def test_1(self, bash): + """The old interface `__expand_tilde_by_ref` should not fail when it is + called without arguments""" assert_bash_exec(bash, "__expand_tilde_by_ref >/dev/null") def test_2(self, bash): """Test environment non-pollution, detected at teardown.""" assert_bash_exec( bash, - '_x() { local aa="~"; __expand_tilde_by_ref aa; }; _x; unset _x', + '_x() { local REPLY; _comp_expand_tilde "~"; }; _x; unset -f _x', + ) + + @pytest.fixture(scope="class") + def functions(self, bash): + # $HOME tinkering: protect against $HOME != ~user; our "home" is the + # latter but plain_tilde follows $HOME + assert_bash_exec( + bash, + '_comp__test_unit() { local REPLY HOME=$1; _comp_expand_tilde "$2"; printf "%s\\n" "$REPLY"; }', ) @pytest.mark.parametrize("plain_tilde", (True, False)) @@ -28,19 +39,21 @@ class TestUnitExpandTildeByRef: ("/a;echo hello", True), ), ) - def test_expand(self, bash, user_home, plain_tilde, suffix_expanded): + def test_expand( + self, bash, user_home, plain_tilde, suffix_expanded, functions + ): user, home = user_home suffix, expanded = suffix_expanded + home2 = home if plain_tilde: user = "" if not suffix or not expanded: - home = "~" + home2 = "~" elif not expanded: - home = "~%s" % user + home2 = "~%s" % user output = assert_bash_exec( bash, - r'var="~%s%s"; __expand_tilde_by_ref var; printf "%%s\n" "$var"' - % (user, suffix), + r'_comp__test_unit "%s" "~%s%s"' % (home, user, suffix), want_output=True, ) - assert output.strip() == "%s%s" % (home, suffix.replace(r"\$", "$"),) + assert output.strip() == "%s%s" % (home2, suffix.replace(r"\$", "$")) diff --git a/test/t/unit/test_unit_filedir.py b/test/t/unit/test_unit_filedir.py index b847efc..07ae2f3 100644 --- a/test/t/unit/test_unit_filedir.py +++ b/test/t/unit/test_unit_filedir.py @@ -15,18 +15,18 @@ class TestUnitFiledir: def functions(self, request, bash): assert_bash_exec( bash, - "_f() { local cur=$(_get_cword); unset COMPREPLY; _filedir; }; " + "_f() { local cur;_comp_get_words cur; unset -v COMPREPLY; _comp_compgen_filedir; }; " "complete -F _f f; " "complete -F _f -o filenames f2", ) assert_bash_exec( bash, - "_g() { local cur=$(_get_cword); unset COMPREPLY; _filedir e1; }; " + "_g() { local cur;_comp_get_words cur; unset -v COMPREPLY; _comp_compgen_filedir e1; }; " "complete -F _g g", ) assert_bash_exec( bash, - "_fd() { local cur=$(_get_cword); unset COMPREPLY; _filedir -d; };" + "_fd() { local cur;_comp_get_words cur; unset -v COMPREPLY; _comp_compgen_filedir -d; };" "complete -F _fd fd", ) @@ -59,7 +59,7 @@ class TestUnitFiledir: return lc_ctype def test_1(self, bash): - assert_bash_exec(bash, "_filedir >/dev/null") + assert_bash_exec(bash, "_comp_compgen_filedir >/dev/null") @pytest.mark.parametrize("funcname", "f f2".split()) def test_2(self, bash, functions, funcname): @@ -196,7 +196,7 @@ class TestUnitFiledir: @pytest.mark.parametrize("funcname", "f f2".split()) def test_22(self, bash, functions, funcname, non_windows_testdir): completion = assert_complete( - bash, r"%s '%s/a\b/" % (funcname, non_windows_testdir) + bash, rf"{funcname} '{non_windows_testdir}/a\b/" ) assert completion == "g'" diff --git a/test/t/unit/test_unit_get_cword.py b/test/t/unit/test_unit_get_cword.py index 0b56d16..d2bb526 100644 --- a/test/t/unit/test_unit_get_cword.py +++ b/test/t/unit/test_unit_get_cword.py @@ -1,11 +1,12 @@ -import pexpect +import pexpect # type: ignore[import] import pytest from conftest import PS1, TestUnitBase, assert_bash_exec @pytest.mark.bashcomp( - cmd=None, ignore_env=r"^[+-](COMP_(WORDS|CWORD|LINE|POINT)|_scp_path_esc)=" + cmd=None, + ignore_env=r"^[+-](COMP_(WORDS|CWORD|LINE|POINT)|_comp_cmd_scp__path_esc)=", ) class TestUnitGetCword(TestUnitBase): def _test(self, *args, **kwargs): @@ -49,12 +50,12 @@ class TestUnitGetCword(TestUnitBase): assert output == r"b\ c" def test_8(self, bash): - r"""a b\| c should return b\ """ + r"""a b\| c should return b\ """ # fmt: skip output = self._test(bash, r"(a 'b\ c')", 1, r"a b\ c", 4) assert output == "b\\" def test_9(self, bash): - r"""a "b\| should return "b\ """ + r"""a "b\| should return "b\ """ # fmt: skip output = self._test(bash, "(a '\"b\\')", 1, r"a \"b\\", 5) assert output == '"b\\' @@ -103,7 +104,7 @@ class TestUnitGetCword(TestUnitBase): a -n| should return -n This test makes sure `_get_cword' doesn't use `echo' to return its - value, because -n might be interpreted by `echo' and thus woud not + value, because -n might be interpreted by `echo' and thus would not be returned. """ output = self._test(bash, "(a -n)", 1, "a -n", 4) @@ -152,3 +153,12 @@ class TestUnitGetCword(TestUnitBase): ] ) assert got == 1 + + def test_25(self, bash): + """ + a b c:| with trailing whitespace after the caret (no more words) and + with WORDBREAKS -= : should return c: + """ + assert_bash_exec(bash, "add_comp_wordbreak_char :") + output = self._test(bash, "(a b c :)", 3, "a b c: ", 6, arg=":") + assert output == "c:" diff --git a/test/t/unit/test_unit_get_first_arg.py b/test/t/unit/test_unit_get_first_arg.py new file mode 100644 index 0000000..415e217 --- /dev/null +++ b/test/t/unit/test_unit_get_first_arg.py @@ -0,0 +1,90 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitGetFirstArg: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_unit() { local -a "words=$1"; local cword=$2 REPLY=; shift 2; _comp_get_first_arg "$@" && printf "%s\\n" "$REPLY"; return 0; }', + ) + + def _test(self, bash, words, cword, args=""): + return assert_bash_exec( + bash, + '_comp__test_unit "%s" %d %s' % (words, cword, args), + want_output=None, + ).strip() + + def test_1(self, bash, functions): + assert_bash_exec(bash, "_comp__test_unit '()' 0") + + def test_2(self, bash, functions): + output = self._test(bash, "(a b)", 2) + assert output == "b" + + def test_3(self, bash, functions): + output = self._test(bash, "(a bc)", 2) + assert output == "bc" + + def test_4(self, bash, functions): + output = self._test(bash, "(a b c)", 2) + assert output == "b" + + def test_5(self, bash, functions): + """Neither of the current word and the command name should be picked + as the first argument""" + output = self._test(bash, "(a b c)", 1) + assert output == "" + + def test_6(self, bash, functions): + """Options starting with - should not be picked as the first + argument""" + output = self._test(bash, "(a -b -c d e)", 4) + assert output == "d" + + def test_7_single_hyphen(self, bash, functions): + """- should be counted as an argument representing stdout/stdin""" + output = self._test(bash, "(a -b - c -d e)", 5) + assert output == "-" + + def test_8_double_hyphen_1(self, bash, functions): + """any word after -- should be picked""" + output = self._test(bash, "(a -b -- -c -d e)", 5) + assert output == "-c" + + def test_8_double_hyphen_2(self, bash, functions): + """any word after -- should be picked only without any preceding argument""" + output = self._test(bash, "(a b -- -c -d e)", 5) + assert output == "b" + + def test_9_skip_optarg_1(self, bash, functions): + output = self._test(bash, "(a -b -c d e f)", 5, '-a "@(-c|--foo)"') + assert output == "e" + + def test_9_skip_optarg_2(self, bash, functions): + output = self._test(bash, "(a -b --foo d e f)", 5, '-a "@(-c|--foo)"') + assert output == "e" + + def test_9_skip_optarg_3(self, bash): + output = self._test(bash, "(a -b - c d e)", 5, '-a "-b"') + assert output == "c" + + def test_9_skip_optarg_4(self, bash): + output = self._test(bash, "(a -b -c d e f)", 5, '-a "-[bc]"') + assert output == "d" + + def test_9_skip_optarg_5(self, bash): + output = self._test(bash, "(a +o b c d)", 4, '-a "+o"') + assert output == "c" + + def test_9_skip_optarg_6(self, bash): + output = self._test(bash, "(a -o -o -o -o b c)", 6, '-a "-o"') + assert output == "b" + + def test_9_skip_optarg_7(self, bash): + output = self._test(bash, "(a -o -- -b -c d e)", 6, '-a "-o"') + assert output == "d" diff --git a/test/t/unit/test_unit_get_comp_words_by_ref.py b/test/t/unit/test_unit_get_words.py index b6498fa..63c4034 100644 --- a/test/t/unit/test_unit_get_comp_words_by_ref.py +++ b/test/t/unit/test_unit_get_words.py @@ -11,10 +11,10 @@ class TestUnitGetCompWordsByRef(TestUnitBase): def _test(self, bash, *args, **kwargs): assert_bash_exec(bash, "unset cur prev") output = self._test_unit( - "_get_comp_words_by_ref %s cur prev; echo $cur,${prev-}", + "_comp_get_words %s cur prev; echo $cur,${prev-}", bash, *args, - **kwargs + **kwargs, ) return output.strip() @@ -22,7 +22,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): assert_bash_exec( bash, "COMP_WORDS=() COMP_CWORD= COMP_POINT= COMP_LINE= " - "_get_comp_words_by_ref cur >/dev/null", + "_comp_get_words cur >/dev/null", ) def test_2(self, bash): @@ -41,12 +41,12 @@ class TestUnitGetCompWordsByRef(TestUnitBase): assert output == "," def test_5(self, bash): - """|a """ + """|a """ # fmt: skip output = self._test(bash, "(a)", 0, "a ", 0) assert output == "," def test_6(self, bash): - """ | a """ + """ | a """ # fmt: skip output = self._test(bash, "(a)", 0, " a ", 1) assert output.strip() == "," @@ -134,9 +134,9 @@ class TestUnitGetCompWordsByRef(TestUnitBase): def test_23(self, bash): """a -n| - This test makes sure `_get_cword' doesn't use `echo' to return its - value, because -n might be interpreted by `echo' and thus woud not - be returned. + This test makes sure `_comp_get_words' doesn't use `echo' to + return its value, because -n might be interpreted by `echo' + and thus would not be returned. """ output = self._test(bash, "(a -n)", 1, "a -n", 4) assert output == "-n,a" @@ -175,7 +175,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b| to all vars""" assert_bash_exec(bash, "unset words cword cur prev") output = self._test_unit( - "_get_comp_words_by_ref words cword cur prev%s; " + "_comp_get_words words cword cur prev%s; " 'echo "${words[@]}",$cword,$cur,$prev', bash, "(a b)", @@ -189,7 +189,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b| to alternate vars""" assert_bash_exec(bash, "unset words2 cword2 cur2 prev2") output = self._test_unit( - "_get_comp_words_by_ref -w words2 -i cword2 -c cur2 -p prev2%s; " + "_comp_get_words -w words2 -i cword2 -c cur2 -p prev2%s; " 'echo $cur2,$prev2,"${words2[@]}",$cword2', bash, "(a b)", @@ -204,7 +204,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b : c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a b : c)", 3, @@ -217,7 +217,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b: c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a b : c)", 3, @@ -230,7 +230,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): """a b :c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a b : c)", 3, @@ -243,7 +243,7 @@ class TestUnitGetCompWordsByRef(TestUnitBase): r"""a b\ :c| with wordbreaks -= :""" assert_bash_exec(bash, "unset words") output = self._test_unit( - '_get_comp_words_by_ref -n : words%s; echo "${words[@]}"', + '_comp_get_words -n : words%s; echo "${words[@]}"', bash, "(a 'b ' : c)", 3, @@ -255,6 +255,6 @@ class TestUnitGetCompWordsByRef(TestUnitBase): def test_unknown_arg_error(self, bash): with pytest.raises(AssertionError) as ex: _ = assert_bash_exec( - bash, "_get_comp_words_by_ref dummy", want_output=True + bash, "_comp_get_words dummy", want_output=True ) ex.match("dummy.* unknown argument") diff --git a/test/t/unit/test_unit_init_completion.py b/test/t/unit/test_unit_init_completion.py deleted file mode 100644 index 64a5a79..0000000 --- a/test/t/unit/test_unit_init_completion.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from conftest import TestUnitBase, assert_bash_exec, assert_complete - - -@pytest.mark.bashcomp( - cmd=None, - ignore_env=r"^[+-](COMP(_(WORDS|CWORD|LINE|POINT)|REPLY)|" - r"cur|cword|words)=", -) -class TestUnitInitCompletion(TestUnitBase): - def test_1(self, bash): - """Test environment non-pollution, detected at teardown.""" - assert_bash_exec( - bash, - "foo() { " - "local cur prev words cword " - "COMP_WORDS=() COMP_CWORD=0 COMP_LINE= COMP_POINT=0; " - "_init_completion; }; " - "foo; unset foo", - ) - - def test_2(self, bash): - output = self._test_unit( - "_init_completion %s; echo $cur,${prev-}", bash, "(a)", 0, "a", 0 - ) - assert output == "," - - @pytest.mark.parametrize("redirect", "> >> 2> < &>".split()) - def test_redirect(self, bash, redirect): - completion = assert_complete( - bash, "%s " % redirect, cwd="shared/default" - ) - assert all(x in completion for x in "foo bar".split()) diff --git a/test/t/unit/test_unit_initialize.py b/test/t/unit/test_unit_initialize.py new file mode 100644 index 0000000..63fddee --- /dev/null +++ b/test/t/unit/test_unit_initialize.py @@ -0,0 +1,66 @@ +import pytest + +from conftest import TestUnitBase, assert_bash_exec, assert_complete + + +@pytest.mark.bashcomp( + cmd=None, + ignore_env=r"^[+-](COMP(_(WORDS|CWORD|LINE|POINT)|REPLY)|" + r"cur|prev|cword|words)=|^\+declare -f _cmd1$", +) +class TestUnitInitCompletion(TestUnitBase): + def test_1(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { " + "local cur prev words cword comp_args " + "COMP_WORDS=() COMP_CWORD=0 COMP_LINE= COMP_POINT=0; " + "_comp_initialize; }; " + "foo; unset -f foo", + ) + + def test_2(self, bash): + output = self._test_unit( + "_comp_initialize %s; echo $cur,${prev-}", bash, "(a)", 0, "a", 0 + ) + assert output == "," + + @pytest.mark.parametrize("redirect", "> >> 2> < &>".split()) + def test_redirect(self, bash, redirect): + completion = assert_complete( + bash, "%s " % redirect, cwd="shared/default" + ) + assert all(x in completion for x in "foo bar".split()) + + @pytest.fixture(scope="class") + def cmd1_empty_completion_setup(self, bash): + assert_bash_exec( + bash, + '_cmd1() { local cur prev words cword comp_args; _comp_initialize -- "$@"; } && ' + "complete -F _cmd1 cmd1", + ) + + @pytest.mark.parametrize("redirect", "> >> 2> {fd1}> < &> &>> >|".split()) + def test_redirect_2(self, bash, cmd1_empty_completion_setup, redirect): + # Note: Bash 4.3 and below cannot properly extract the redirection ">|" + if redirect == ">|": + skipif = "((BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1] < 404))" + try: + assert_bash_exec(bash, skipif, want_output=None) + except AssertionError: + pass + else: + pytest.skip(skipif) + + completion = assert_complete( + bash, "cmd1 %s f" % redirect, cwd="shared/default" + ) + assert "foo" in completion + + @pytest.mark.parametrize("redirect", "> >> 2> < &>".split()) + def test_redirect_3(self, bash, redirect): + completion = assert_complete( + bash, "cmd1 %sf" % redirect, cwd="shared/default" + ) + assert "foo" in completion diff --git a/test/t/unit/test_unit_ip_addresses.py b/test/t/unit/test_unit_ip_addresses.py index 8120c88..37f3b0e 100644 --- a/test/t/unit/test_unit_ip_addresses.py +++ b/test/t/unit/test_unit_ip_addresses.py @@ -9,41 +9,41 @@ class TestUnitIpAddresses: def functions(self, request, bash): assert_bash_exec( bash, - "_ia() { local cur=$(_get_cword);unset COMPREPLY;" - "_ip_addresses; }", + "_ia() { local cur;_comp_get_words cur;" + "unset -v COMPREPLY;_comp_compgen_ip_addresses; }", ) assert_bash_exec(bash, "complete -F _ia ia") assert_bash_exec( bash, - "_iaa() { local cur=$(_get_cword);unset COMPREPLY;" - "_ip_addresses -a; }", + "_iaa() { local cur;_comp_get_words cur;" + "unset -v COMPREPLY;_comp_compgen_ip_addresses -a; }", ) assert_bash_exec(bash, "complete -F _iaa iaa") assert_bash_exec( bash, - " _ia6() { local cur=$(_get_cword);unset COMPREPLY;" - "_ip_addresses -6; }", + " _ia6() { local cur;_comp_get_words cur;" + "unset -v COMPREPLY;_comp_compgen_ip_addresses -6; }", ) assert_bash_exec(bash, "complete -F _ia6 ia6") def test_1(self, bash): - assert_bash_exec(bash, "_ip_addresses") + assert_bash_exec(bash, "_comp_compgen_ip_addresses") @pytest.mark.complete("iaa ") def test_2(self, functions, completion): - """_ip_addresses -a should complete ip addresses.""" + """_comp_compgen_ip_addresses -a should complete ip addresses.""" assert completion assert all("." in x or ":" in x for x in completion) @pytest.mark.complete("ia ") def test_3(self, functions, completion): - """_ip_addresses should complete ipv4 addresses.""" + """_comp_compgen_ip_addresses should complete ipv4 addresses.""" assert completion assert all("." in x for x in completion) @pytest.mark.xfail(in_container(), reason="Probably fails in a container") @pytest.mark.complete("ia6 ") def test_4(self, functions, completion): - """_ip_addresses -6 should complete ipv6 addresses.""" + """_comp_compgen_ip_addresses -6 should complete ipv6 addresses.""" assert completion assert all(":" in x for x in completion) diff --git a/test/t/unit/test_unit_known_hosts_real.py b/test/t/unit/test_unit_known_hosts.py index ac5205e..b0f715b 100644 --- a/test/t/unit/test_unit_known_hosts_real.py +++ b/test/t/unit/test_unit_known_hosts.py @@ -2,14 +2,14 @@ from itertools import chain import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp( cmd=None, - ignore_env="^[+-](COMP(REPLY|_KNOWN_HOSTS_WITH_HOSTFILE)|OLDHOME)=", + ignore_env="^[+-](COMPREPLY|BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE)=", ) -class TestUnitKnownHostsReal: +class TestUnitCompgenKnownHosts: @pytest.mark.parametrize( "prefix,colon_flag,hostfile", [("", "", True), ("", "", False), ("user@", "c", True)], @@ -21,9 +21,9 @@ class TestUnitKnownHostsReal: "%s%s%s" % (prefix, x, ":" if colon_flag else "") for x in chain( hosts if hostfile else avahi_hosts, - # fixtures/_known_hosts_real/config + # fixtures/_known_hosts/config "gee hus jar #not-a-comment".split(), - # fixtures/_known_hosts_real/known_hosts + # fixtures/_known_hosts/known_hosts ( "doo", "ike", @@ -43,14 +43,14 @@ class TestUnitKnownHostsReal: ) assert_bash_exec( bash, - "unset -v COMP_KNOWN_HOSTS_WITH_HOSTFILE" + "unset -v BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE" if hostfile - else "COMP_KNOWN_HOSTS_WITH_HOSTFILE=", + else "BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE=", ) output = assert_bash_exec( bash, - "_known_hosts_real -a%sF _known_hosts_real/config '%s'; " - r'printf "%%s\n" "${COMPREPLY[@]}"; unset COMPREPLY' + "_comp_compgen_known_hosts -a%sF _known_hosts/config '%s'; " + r'printf "%%s\n" "${COMPREPLY[@]}"; unset -v COMPREPLY' % (colon_flag, prefix), want_output=True, ) @@ -66,12 +66,13 @@ class TestUnitKnownHostsReal: ) def test_ip_filtering(self, bash, family, result): assert_bash_exec( - bash, "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE" + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE", ) output = assert_bash_exec( bash, - "COMP_KNOWN_HOSTS_WITH_HOSTFILE= " - "_known_hosts_real -%sF _known_hosts_real/localhost_config ''; " + "BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE= " + "_comp_compgen_known_hosts -%sF _known_hosts/localhost_config ''; " r'printf "%%s\n" "${COMPREPLY[@]}"' % family, want_output=True, ) @@ -79,17 +80,17 @@ class TestUnitKnownHostsReal: def test_consecutive_spaces(self, bash, hosts): expected = hosts.copy() - # fixtures/_known_hosts_real/spaced conf + # fixtures/_known_hosts/spaced conf expected.extend("gee hus #not-a-comment".split()) - # fixtures/_known_hosts_real/known_hosts2 + # fixtures/_known_hosts/known_hosts2 expected.extend("two two2 two3 two4".split()) # fixtures/_known_hosts_/spaced known_hosts expected.extend("doo ike".split()) output = assert_bash_exec( bash, - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF '_known_hosts_real/spaced conf' ''; " + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE; " + "_comp_compgen_known_hosts -aF '_known_hosts/spaced conf' ''; " r'printf "%s\n" "${COMPREPLY[@]}"', want_output=True, ) @@ -97,62 +98,58 @@ class TestUnitKnownHostsReal: def test_files_starting_with_tilde(self, bash, hosts): expected = hosts.copy() - # fixtures/_known_hosts_real/known_hosts2 + # fixtures/_known_hosts/known_hosts2 expected.extend("two two2 two3 two4".split()) - # fixtures/_known_hosts_real/known_hosts3 + # fixtures/_known_hosts/known_hosts3 expected.append("three") - # fixtures/_known_hosts_real/known_hosts4 + # fixtures/_known_hosts/known_hosts4 expected.append("four") - assert_bash_exec(bash, 'OLDHOME="$HOME"; HOME="%s"' % bash.cwd) - output = assert_bash_exec( - bash, - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF _known_hosts_real/config_tilde ''; " - r'printf "%s\n" "${COMPREPLY[@]}"', - want_output=True, - ) - assert_bash_exec(bash, 'HOME="$OLDHOME"') + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("HOME", bash.cwd) + output = assert_bash_exec( + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE;" + " _comp_compgen_known_hosts -aF _known_hosts/config_tilde ''; " + r'printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ) + assert sorted(set(output.strip().split())) == sorted(expected) def test_included_configs(self, bash, hosts): expected = hosts.copy() - # fixtures/_known_hosts_real/config_include_recursion + # fixtures/_known_hosts/config_include_recursion expected.append("recursion") - # fixtures/_known_hosts_real/.ssh/config_relative_path + # fixtures/_known_hosts/.ssh/config_relative_path expected.append("relative_path") - # fixtures/_known_hosts_real/.ssh/config_asterisk_* + # fixtures/_known_hosts/.ssh/config_asterisk_* expected.extend("asterisk_1 asterisk_2".split()) - # fixtures/_known_hosts_real/.ssh/config_question_mark + # fixtures/_known_hosts/.ssh/config_question_mark expected.append("question_mark") - assert_bash_exec( - bash, 'OLDHOME="$HOME"; HOME="%s/_known_hosts_real"' % bash.cwd - ) - output = assert_bash_exec( - bash, - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF _known_hosts_real/config_include ''; " - r'printf "%s\n" "${COMPREPLY[@]}"', - want_output=True, - ) - assert_bash_exec(bash, 'HOME="$OLDHOME"') + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("HOME", "%s/_known_hosts" % bash.cwd) + output = assert_bash_exec( + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE;" + " _comp_compgen_known_hosts -aF _known_hosts/config_include ''; " + r'printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ) assert sorted(set(output.strip().split())) == sorted(expected) def test_no_globbing(self, bash): - assert_bash_exec( - bash, 'OLDHOME="$HOME"; HOME="%s/_known_hosts_real"' % bash.cwd - ) - output = assert_bash_exec( - bash, - "cd _known_hosts_real; " - "unset -v COMPREPLY COMP_KNOWN_HOSTS_WITH_HOSTFILE; " - "_known_hosts_real -aF config ''; " - r'printf "%s\n" "${COMPREPLY[@]}"; ' - "cd - &>/dev/null", - want_output=True, - ) - assert_bash_exec(bash, 'HOME="$OLDHOME"') + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("HOME", "%s/_known_hosts" % bash.cwd) + bash_env.chdir("_known_hosts") + output = assert_bash_exec( + bash, + "unset -v COMPREPLY BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE;" + " _comp_compgen_known_hosts -aF config ''; " + r'printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ) completion = sorted(set(output.strip().split())) assert "gee" in completion assert "gee-filename-canary" not in completion diff --git a/test/t/unit/test_unit_load_completion.py b/test/t/unit/test_unit_load_completion.py new file mode 100644 index 0000000..6272e5b --- /dev/null +++ b/test/t/unit/test_unit_load_completion.py @@ -0,0 +1,94 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, cwd="_comp_load") +class TestLoadCompletion: + def test_userdir_1(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", + "$PWD/userdir1:$PWD/userdir2:$BASH_COMPLETION_USER_DIR", + quote=False, + ) + bash_env.write_variable( + "PATH", "$PWD/prefix1/bin:$PWD/prefix1/sbin", quote=False + ) + output = assert_bash_exec( + bash, "_comp_load cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from userdir1" + output = assert_bash_exec( + bash, "_comp_load cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from userdir2" + + def test_PATH_1(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "PATH", "$PWD/prefix1/bin:$PWD/prefix1/sbin", quote=False + ) + output = assert_bash_exec( + bash, "_comp_load cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + output = assert_bash_exec( + bash, "complete -p cmd2", want_output=True + ) + assert " cmd2" in output + output = assert_bash_exec( + bash, 'complete -p "$PWD/prefix1/sbin/cmd2"', want_output=True + ) + assert "/prefix1/sbin/cmd2" in output + + def test_cmd_path_1(self, bash): + assert_bash_exec(bash, "complete -r cmd1 || :", want_output=None) + output = assert_bash_exec( + bash, "_comp_load prefix1/bin/cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, 'complete -p "$PWD/prefix1/bin/cmd1"', want_output=True + ) + assert "/prefix1/bin/cmd1" in output + assert_bash_exec(bash, "! complete -p cmd1", want_output=None) + output = assert_bash_exec( + bash, "_comp_load prefix1/sbin/cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load bin/cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load bin/cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + + def test_cmd_path_2(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("PATH", "$PWD/bin:$PATH", quote=False) + output = assert_bash_exec( + bash, "_comp_load cmd1", want_output=True + ) + assert output.strip() == "cmd1: sourced from prefix1" + output = assert_bash_exec( + bash, "_comp_load cmd2", want_output=True + ) + assert output.strip() == "cmd2: sourced from prefix1" + + def test_cmd_intree_precedence(self, bash): + """ + Test in-tree, i.e. completions/$cmd relative to the main script + has precedence over location derived from PATH. + """ + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("PATH", "$PWD/prefix1/bin", quote=False) + # The in-tree `sh` completion should be loaded here, + # and cause no output, unlike our `$PWD/prefix1/bin/sh` canary. + assert_bash_exec(bash, "_comp_load sh", want_output=False) diff --git a/test/t/unit/test_unit_longopt.py b/test/t/unit/test_unit_longopt.py index c5488e3..ab823d4 100644 --- a/test/t/unit/test_unit_longopt.py +++ b/test/t/unit/test_unit_longopt.py @@ -10,9 +10,9 @@ class TestUnitLongopt: @pytest.fixture(scope="class") def functions(self, request, bash): assert_bash_exec(bash, "_grephelp() { cat _longopt/grep--help.txt; }") - assert_bash_exec(bash, "complete -F _longopt _grephelp") + assert_bash_exec(bash, "complete -F _comp_complete_longopt _grephelp") assert_bash_exec(bash, "_various() { cat _longopt/various.txt; }") - assert_bash_exec(bash, "complete -F _longopt _various") + assert_bash_exec(bash, "complete -F _comp_complete_longopt _various") @pytest.mark.complete("_grephelp --") def test_1(self, functions, completion): diff --git a/test/t/unit/test_unit_looks_like_path.py b/test/t/unit/test_unit_looks_like_path.py new file mode 100644 index 0000000..3e86b48 --- /dev/null +++ b/test/t/unit/test_unit_looks_like_path.py @@ -0,0 +1,33 @@ +import shlex + +import pytest + +from conftest import TestUnitBase, assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitQuote(TestUnitBase): + @pytest.mark.parametrize( + "thing_looks_like", + ( + ("", False), + ("foo", False), + ("/foo", True), + ("foo/", True), + ("foo/bar", True), + (".", True), + ("../", True), + ("~", True), + ("~foo", True), + ), + ) + def test_1(self, bash, thing_looks_like): + thing, looks_like = thing_looks_like + output = assert_bash_exec( + bash, + f"_comp_looks_like_path {shlex.quote(thing)}; printf %s $?", + want_output=True, + want_newline=False, + ) + is_zero = output.strip() == "0" + assert (looks_like and is_zero) or (not looks_like and not is_zero) diff --git a/test/t/unit/test_unit_parse_help.py b/test/t/unit/test_unit_parse_help.py index 4a02155..1a46f3f 100644 --- a/test/t/unit/test_unit_parse_help.py +++ b/test/t/unit/test_unit_parse_help.py @@ -2,29 +2,29 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+declare -f fn$") class TestUnitParseHelp: def test_1(self, bash): assert_bash_exec(bash, "fn() { echo; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_2(self, bash): assert_bash_exec(bash, "fn() { echo 'no dashes here'; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_3(self, bash): assert_bash_exec(bash, "fn() { echo 'internal-dash'; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_4(self, bash): assert_bash_exec(bash, "fn() { echo 'no -leading-dashes'; }") - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_5(self, bash): @@ -94,6 +94,20 @@ class TestUnitParseHelp: output = assert_bash_exec(bash, "_parse_help fn", want_output=True) assert output.split() == "--foo".split() + def test_17_failglob(self, bash): + assert_bash_exec(bash, "fn() { echo '--foo[=bar]'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec(bash, "_parse_help fn", want_output=True) + assert output.split() == "--foo".split() + + def test_17_nullglob(self, bash): + assert_bash_exec(bash, "fn() { echo '--foo[=bar]'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec(bash, "_parse_help fn", want_output=True) + assert output.split() == "--foo".split() + def test_18(self, bash): assert_bash_exec(bash, "fn() { echo '--foo=<bar>'; }") output = assert_bash_exec(bash, "_parse_help fn", want_output=True) @@ -144,6 +158,13 @@ class TestUnitParseHelp: output = assert_bash_exec(bash, "_parse_help fn", want_output=True) assert output.split() == "--foo".split() + def test_27_middle_dot(self, bash): + """We do not want to include the period at the end of the sentence but + want to include dots connecting names.""" + assert_bash_exec(bash, "fn() { echo '--foo.bar.'; }") + output = assert_bash_exec(bash, "_parse_help fn", want_output=True) + assert output.split() == "--foo.bar".split() + def test_28(self, bash): assert_bash_exec(bash, "fn() { echo '-f or --foo'; }") output = assert_bash_exec(bash, "_parse_help fn", want_output=True) @@ -161,7 +182,7 @@ class TestUnitParseHelp: assert_bash_exec( bash, r"fn() { printf '%s\n' $'----\n---foo\n----- bar'; }" ) - output = assert_bash_exec(bash, "_parse_help fn") + output = assert_bash_exec(bash, "_parse_help fn; (($? == 1))") assert not output def test_31(self, bash): @@ -181,3 +202,33 @@ class TestUnitParseHelp: ) output = assert_bash_exec(bash, "_parse_help fn", want_output=True) assert output.split() == "--exclude=".split() + + def test_custom_helpopt1(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == -h ]] && echo '-option'; }") + output = assert_bash_exec(bash, "_parse_help fn -h", want_output=True) + assert output.split() == "-option".split() + + def test_custom_helpopt2(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == '-?' ]] && echo '-option'; }") + output = assert_bash_exec( + bash, "_parse_help fn '-?'", want_output=True + ) + assert output.split() == "-option".split() + + def test_custom_helpopt2_failglob(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == '-?' ]] && echo '-option'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "_parse_help fn '-?'", want_output=True + ) + assert output.split() == "-option".split() + + def test_custom_helpopt2_nullglob(self, bash): + assert_bash_exec(bash, "fn() { [[ $1 == '-?' ]] && echo '-option'; }") + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "_parse_help fn '-?'", want_output=True + ) + assert output.split() == "-option".split() diff --git a/test/t/unit/test_unit_parse_usage.py b/test/t/unit/test_unit_parse_usage.py index f0cb711..0106922 100644 --- a/test/t/unit/test_unit_parse_usage.py +++ b/test/t/unit/test_unit_parse_usage.py @@ -1,18 +1,18 @@ import pytest -from conftest import assert_bash_exec +from conftest import assert_bash_exec, bash_env_saved @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+declare -f fn$") class TestUnitParseUsage: def test_1(self, bash): assert_bash_exec(bash, "fn() { echo; }") - output = assert_bash_exec(bash, "_parse_usage fn") + output = assert_bash_exec(bash, "_parse_usage fn; (($? == 1))") assert not output def test_2(self, bash): assert_bash_exec(bash, "fn() { echo 'no dashes here'; }") - output = assert_bash_exec(bash, "_parse_usage fn") + output = assert_bash_exec(bash, "_parse_usage fn; (($? == 1))") assert not output def test_3(self, bash): @@ -59,7 +59,7 @@ class TestUnitParseUsage: assert_bash_exec( bash, "fn() { echo ----; echo ---foo; echo '----- bar'; }" ) - output = assert_bash_exec(bash, "_parse_usage fn") + output = assert_bash_exec(bash, "_parse_usage fn; (($? == 1))") assert not output def test_12(self, bash): @@ -67,3 +67,41 @@ class TestUnitParseUsage: bash, "echo '[-duh]' | _parse_usage -", want_output=True ) assert output.split() == "-d -u -h".split() + + def test_custom_helpopt1(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == -h ]] && echo 'fn [-option]'; true; }" + ) + output = assert_bash_exec(bash, "_parse_usage fn -h", want_output=True) + assert output.split() == "-o -p -t -i -o -n".split() + + def test_custom_helpopt2(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == '-?' ]] && echo 'fn [-option]'; }" + ) + output = assert_bash_exec( + bash, "_parse_usage fn '-?'", want_output=True + ) + assert output.split() == "-o -p -t -i -o -n".split() + + def test_custom_helpopt2_failglob(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == '-?' ]] && echo 'fn [-option]'; }" + ) + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "_parse_usage fn '-?'", want_output=True + ) + assert output.split() == "-o -p -t -i -o -n".split() + + def test_custom_helpopt2_nullglob(self, bash): + assert_bash_exec( + bash, "fn() { [[ $1 == '-?' ]] && echo 'fn [-option]'; }" + ) + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "_parse_usage fn '-?'", want_output=True + ) + assert output.split() == "-o -p -t -i -o -n".split() diff --git a/test/t/unit/test_unit_pgids.py b/test/t/unit/test_unit_pgids.py new file mode 100644 index 0000000..05019ec --- /dev/null +++ b/test/t/unit/test_unit_pgids.py @@ -0,0 +1,34 @@ +import os + +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+(COMPREPLY|cur)=") +class TestUnitPgids: + def test_smoke(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + assert_bash_exec(bash, "_comp_compgen_pgids >/dev/null") + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local cur=; _comp_compgen_pgids; }; foo; unset -f foo", + ) + + def test_ints(self, bash): + """Test that we get something sensible, and only int'y strings.""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + completion = assert_bash_exec( + bash, + r'_comp_compgen_pgids; printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ).split() + assert completion + if hasattr(os, "getpgid"): + assert str(os.getpgid(0)) in completion + assert all(x.isdigit() for x in completion) diff --git a/test/t/unit/test_unit_pids.py b/test/t/unit/test_unit_pids.py new file mode 100644 index 0000000..3681c8c --- /dev/null +++ b/test/t/unit/test_unit_pids.py @@ -0,0 +1,34 @@ +import os + +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") +class TestUnitPids: + def test_smoke(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + assert_bash_exec(bash, "_comp_compgen_pids >/dev/null") + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local cur=; _comp_compgen_pids; }; foo; unset -f foo", + ) + + def test_ints(self, bash): + """Test that we get something sensible, and only int'y strings.""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + completion = assert_bash_exec( + bash, + r'_comp_compgen_pids; printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ).split() + assert completion + if hasattr(os, "getpid"): + assert str(os.getpid()) in completion + assert all(x.isdigit() for x in completion) diff --git a/test/t/unit/test_unit_pnames.py b/test/t/unit/test_unit_pnames.py new file mode 100644 index 0000000..e0819e5 --- /dev/null +++ b/test/t/unit/test_unit_pnames.py @@ -0,0 +1,29 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") +class TestUnitPnames: + def test_smoke(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + assert_bash_exec(bash, "_comp_compgen_pnames >/dev/null") + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local cur=; _comp_compgen_pnames; }; foo; unset -f foo", + ) + + def test_something(self, bash): + """Test that we get something.""" + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("cur", "") + completion = assert_bash_exec( + bash, + r'_comp_compgen_pids; printf "%s\n" "${COMPREPLY[@]}"', + want_output=True, + ).split() + assert completion diff --git a/test/t/unit/test_unit_quote.py b/test/t/unit/test_unit_quote.py index b280bd6..3508782 100644 --- a/test/t/unit/test_unit_quote.py +++ b/test/t/unit/test_unit_quote.py @@ -3,34 +3,41 @@ import pytest from conftest import TestUnitBase, assert_bash_exec -@pytest.mark.bashcomp(cmd=None) +@pytest.mark.bashcomp( + cmd=None, + ignore_env=r"^\+declare -f __tester$", +) class TestUnitQuote(TestUnitBase): def test_1(self, bash): + assert_bash_exec( + bash, + '__tester() { local REPLY; _comp_quote "$1"; printf %s "$REPLY"; }', + ) output = assert_bash_exec( - bash, 'quote "a b"', want_output=True, want_newline=False + bash, '__tester "a b"', want_output=True, want_newline=False ) assert output.strip() == "'a b'" def test_2(self, bash): output = assert_bash_exec( - bash, 'quote "a b"', want_output=True, want_newline=False + bash, '__tester "a b"', want_output=True, want_newline=False ) assert output.strip() == "'a b'" def test_3(self, bash): output = assert_bash_exec( - bash, 'quote " a "', want_output=True, want_newline=False + bash, '__tester " a "', want_output=True, want_newline=False ) assert output.strip() == "' a '" def test_4(self, bash): output = assert_bash_exec( - bash, "quote \"a'b'c\"", want_output=True, want_newline=False + bash, "__tester \"a'b'c\"", want_output=True, want_newline=False ) assert output.strip() == r"'a'\''b'\''c'" def test_5(self, bash): output = assert_bash_exec( - bash, 'quote "a\'"', want_output=True, want_newline=False + bash, '__tester "a\'"', want_output=True, want_newline=False ) assert output.strip() == r"'a'\'''" diff --git a/test/t/unit/test_unit_quote_compgen.py b/test/t/unit/test_unit_quote_compgen.py new file mode 100644 index 0000000..faf23fe --- /dev/null +++ b/test/t/unit/test_unit_quote_compgen.py @@ -0,0 +1,173 @@ +import os + +import pytest + +from conftest import assert_bash_exec, assert_complete, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None, temp_cwd=True) +class TestUnitQuoteCompgen: + @pytest.fixture(scope="class") + def functions(self, bash): + assert_bash_exec( + bash, + '_comp__test_quote_compgen() { local REPLY; _comp_quote_compgen "$1"; printf %s "$REPLY"; }', + ) + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_exec(self, bash, functions, funcname): + assert_bash_exec(bash, "%s '' >/dev/null" % funcname) + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_env_non_pollution(self, bash, functions, funcname): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, "foo() { %s meh >/dev/null; }; foo; unset -f foo" % funcname + ) + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_1(self, bash, functions, funcname): + output = assert_bash_exec( + bash, "%s '';echo" % funcname, want_output=True + ) + assert output.strip() == "''" + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_2(self, bash, functions, funcname): + output = assert_bash_exec( + bash, "%s foo;echo" % funcname, want_output=True + ) + assert output.strip() == "foo" + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_3(self, bash, functions, funcname): + output = assert_bash_exec( + bash, '%s foo\\"bar;echo' % funcname, want_output=True + ) + assert output.strip() == 'foo\\"bar' + + @pytest.mark.parametrize( + "funcname", "_comp__test_quote_compgen quote_readline".split() + ) + def test_4(self, bash, functions, funcname): + output = assert_bash_exec( + bash, "%s '$(echo x >&2)';echo" % funcname, want_output=True + ) + assert output.strip() == "\\$\\(echo\\ x\\ \\>\\&2\\)" + + def test_github_issue_189_1(self, bash, functions): + """Test error messages on a certain command line + + Reported at https://github.com/scop/bash-completion/issues/189 + + Syntax error messages should not be shown by completion on the + following line: + + $ ls -- '${[TAB] + $ rm -- '${[TAB] + + """ + assert_bash_exec(bash, "_comp__test_quote_compgen $'\\'${' >/dev/null") + + def test_github_issue_492_1(self, bash, functions): + """Test unintended code execution on a certain command line + + Reported at https://github.com/scop/bash-completion/pull/492 + + Arbitrary commands could be unintendedly executed by + _comp_quote_compgen. In the following example, the command "touch + 1.txt" would be unintendedly created before the fix. The file "1.txt" + should not be created by completion on the following line: + + $ echo '$(touch file.txt)[TAB] + + """ + assert_bash_exec( + bash, "_comp__test_quote_compgen $'\\'$(touch 1.txt)' >/dev/null" + ) + assert not os.path.exists("./1.txt") + + def test_github_issue_492_2(self, bash, functions): + """Test the file clear by unintended redirection on a certain command line + + Reported at https://github.com/scop/bash-completion/pull/492 + + The file "1.0" should not be created by completion on the following + line: + + $ awk '$1 > 1.0[TAB] + + """ + assert_bash_exec( + bash, "_comp__test_quote_compgen $'\\'$1 > 1.0' >/dev/null" + ) + assert not os.path.exists("./1.0") + + def test_github_issue_492_3(self, bash, functions): + """Test code execution through unintended pathname expansions + + When there is a file named "quote=$(COMMAND)" (for + _comp_compgen_filedir) or "REPLY=$(COMMAND)" (for _comp_quote_compgen), + the completion of the word '$* results in the execution of COMMAND. + + $ echo '$*[TAB] + + """ + os.mkdir("./REPLY=$(echo injected >&2)") + assert_bash_exec(bash, "_comp__test_quote_compgen $'\\'$*' >/dev/null") + + def test_github_issue_492_4(self, bash, functions): + """Test error messages through unintended pathname expansions + + When "shopt -s failglob" is set by the user, the completion of the word + containing glob character and special characters (e.g. TAB) results in + the failure of pathname expansions. + + $ shopt -s failglob + $ echo a\\ b*[TAB] + + """ + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + assert_bash_exec( + bash, "_comp__test_quote_compgen $'a\\\\\\tb*' >/dev/null" + ) + + def test_github_issue_526_1(self, bash): + r"""Regression tests for unprocessed escape sequences after quotes + + Ref [1] https://github.com/scop/bash-completion/pull/492#discussion_r637213822 + Ref [2] https://github.com/scop/bash-completion/pull/526 + + The escape sequences in the local variable of "value" in + "_comp_quote_compgen" needs to be unescaped by passing it to printf as + the format string. This causes a problem in the following case [where + the spaces after "alpha\" is a TAB character inserted in the command + string by "C-v TAB"]: + + $ echo alpha\ b[TAB] + + """ + os.mkdir("./alpha\tbeta") + assert ( + assert_complete( + # Remark on "rendered_cmd": Bash aligns the last character 'b' + # in the rendered cmd to an "8 x n" boundary using spaces. + # Here, the command string is assumed to start from column 2 + # because the width of PS1 (conftest.PS1 = '/@') is 2, + bash, + "echo alpha\\\026\tb", + rendered_cmd="echo alpha\\ b", + ) + == "eta/" + ) diff --git a/test/t/unit/test_unit_quote_readline.py b/test/t/unit/test_unit_quote_readline.py deleted file mode 100644 index e2b437e..0000000 --- a/test/t/unit/test_unit_quote_readline.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from conftest import assert_bash_exec - - -@pytest.mark.bashcomp(cmd=None) -class TestUnitQuoteReadline: - def test_exec(self, bash): - assert_bash_exec(bash, "quote_readline '' >/dev/null") - - def test_env_non_pollution(self, bash): - """Test environment non-pollution, detected at teardown.""" - assert_bash_exec( - bash, "foo() { quote_readline meh >/dev/null; }; foo; unset foo" - ) diff --git a/test/t/unit/test_unit_realcommand.py b/test/t/unit/test_unit_realcommand.py new file mode 100644 index 0000000..4eb7b73 --- /dev/null +++ b/test/t/unit/test_unit_realcommand.py @@ -0,0 +1,90 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, cwd="shared", ignore_env=r"^\+declare -f __tester$" +) +class TestUnitRealCommand: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, + ( + "__tester() { " + "local REPLY rc; " + '_comp_realcommand "$1"; ' + "rc=$?; " + 'printf %s "$REPLY"; ' + "return $rc; " + "}" + ), + ) + + def test_non_pollution(self, bash): + """Test environment non-pollution, detected at teardown.""" + assert_bash_exec( + bash, + "foo() { local REPLY=; _comp_realcommand bar; }; foo; unset -f foo", + want_output=None, + ) + + def test_basename(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("PATH", "$PWD/bin:$PATH", quote=False) + output = assert_bash_exec( + bash, + "__tester arp", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/bin/arp") + + def test_basename_nonexistent(self, bash, functions): + filename = "non-existent-file-for-bash-completion-tests" + skipif = "! type -P %s" % filename + try: + assert_bash_exec(bash, skipif, want_output=None) + except AssertionError: + pytest.skipif(skipif) + output = assert_bash_exec( + bash, + "! __tester %s" % filename, + want_output=False, + ) + assert output.strip() == "" + + def test_relative(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester bin/arp", + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/bin/arp") + + def test_relative_nonexistent(self, bash, functions): + output = assert_bash_exec( + bash, + "! __tester bin/non-existent", + want_output=False, + ) + assert output.strip() == "" + + def test_absolute(self, bash, functions): + output = assert_bash_exec( + bash, + '__tester "$PWD/bin/arp"', + want_output=True, + want_newline=False, + ) + assert output.strip().endswith("/shared/bin/arp") + + def test_absolute_nonexistent(self, bash, functions): + output = assert_bash_exec( + bash, + '! __tester "$PWD/bin/non-existent"', + want_output=False, + ) + assert output.strip() == "" diff --git a/test/t/unit/test_unit_split.py b/test/t/unit/test_unit_split.py new file mode 100644 index 0000000..d1f228e --- /dev/null +++ b/test/t/unit/test_unit_split.py @@ -0,0 +1,90 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp( + cmd=None, ignore_env=r"^\+declare -f (dump_array|__tester)$" +) +class TestUtilSplit: + @pytest.fixture + def functions(self, bash): + assert_bash_exec( + bash, "dump_array() { printf '<%s>' \"${arr[@]}\"; echo; }" + ) + assert_bash_exec( + bash, + '__tester() { local -a arr=(00); _comp_split "${@:1:$#-1}" arr "${@:$#}"; dump_array; }', + ) + + def test_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester '12 34 56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_2(self, bash, functions): + output = assert_bash_exec( + bash, "__tester $'12\\n34\\n56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_3(self, bash, functions): + output = assert_bash_exec( + bash, "__tester '12:34:56'", want_output=True + ) + assert output.strip() == "<12:34:56>" + + def test_option_F_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -F : '12:34:56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_option_F_2(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -F : '12 34 56'", want_output=True + ) + assert output.strip() == "<12 34 56>" + + def test_option_l_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -l $'12\\n34\\n56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_option_l_2(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -l '12 34 56'", want_output=True + ) + assert output.strip() == "<12 34 56>" + + def test_option_a_1(self, bash, functions): + output = assert_bash_exec( + bash, "__tester -aF : '12:34:56'", want_output=True + ) + assert output.strip() == "<00><12><34><56>" + + def test_protect_from_failglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("failglob", True) + output = assert_bash_exec( + bash, "__tester -F '*' '12*34*56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_protect_from_nullglob(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("nullglob", True) + output = assert_bash_exec( + bash, "__tester -F '*' '12*34*56'", want_output=True + ) + assert output.strip() == "<12><34><56>" + + def test_protect_from_IFS(self, bash, functions): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable("IFS", "34") + output = assert_bash_exec( + bash, "__tester '12 34 56'", want_output=True + ) + assert output.strip() == "<12><34><56>" diff --git a/test/t/unit/test_unit_tilde.py b/test/t/unit/test_unit_tilde.py index 35a4e4c..fae0dd6 100644 --- a/test/t/unit/test_unit_tilde.py +++ b/test/t/unit/test_unit_tilde.py @@ -6,23 +6,24 @@ from conftest import assert_bash_exec @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") class TestUnitTilde: def test_1(self, bash): - assert_bash_exec(bash, "_tilde >/dev/null") + assert_bash_exec(bash, "! _comp_compgen_tilde >/dev/null") def test_2(self, bash): """Test environment non-pollution, detected at teardown.""" assert_bash_exec( - bash, 'foo() { local aa="~"; _tilde "$aa"; }; foo; unset foo' + bash, + 'foo() { local aa="~"; _comp_compgen -c "$aa" tilde; }; foo; unset -f foo', ) def test_3(self, bash): """Test for https://bugs.debian.org/766163""" - assert_bash_exec(bash, "_tilde ~-o") + assert_bash_exec(bash, "! _comp_compgen -c ~-o tilde") def _test_part_full(self, bash, part, full): res = ( assert_bash_exec( bash, - '_tilde "~%s"; echo "${COMPREPLY[@]}"' % part, + '_comp_compgen -c "~%s" tilde; echo "${COMPREPLY[@]}"' % part, want_output=True, ) .strip() diff --git a/test/t/unit/test_unit_unlocal.py b/test/t/unit/test_unit_unlocal.py new file mode 100644 index 0000000..be5ec56 --- /dev/null +++ b/test/t/unit/test_unit_unlocal.py @@ -0,0 +1,18 @@ +import pytest + +from conftest import assert_bash_exec + + +@pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+(VAR=|declare -f foo)") +class TestUnlocal: + def test_1(self, bash): + cmd = ( + "foo() { " + "local VAR=inner; " + "_comp_unlocal VAR; " + "echo $VAR; " + "}; " + "VAR=outer; foo; " + ) + res = assert_bash_exec(bash, cmd, want_output=True).strip() + assert res == "outer" diff --git a/test/t/unit/test_unit_variables.py b/test/t/unit/test_unit_variables.py index d62bc4a..f15d249 100644 --- a/test/t/unit/test_unit_variables.py +++ b/test/t/unit/test_unit_variables.py @@ -11,7 +11,7 @@ class TestUnitVariables: assert_bash_exec( bash, "unset assoc2 && declare -A assoc2=([idx1]=1 [idx2]=2)" ) - assert_bash_exec(bash, "unset ${!___v*} && declare ___var=''") + assert_bash_exec(bash, 'unset ${!___v*} && declare ___var=""') request.addfinalizer( lambda: assert_bash_exec(bash, "unset ___var assoc1 assoc2") ) @@ -28,6 +28,10 @@ class TestUnitVariables: def test_multiple_array_indexes(self, functions, completion): assert completion == "${assoc2[idx1]} ${assoc2[idx2]}".split() + @pytest.mark.complete(": ${assoc2[", shopt=dict(failglob=True)) + def test_multiple_array_indexes_failglob(self, functions, completion): + assert completion == "${assoc2[idx1]} ${assoc2[idx2]}".split() + @pytest.mark.complete(": ${assoc1[bogus]") def test_closing_curly_after_square(self, functions, completion): assert completion == "}" diff --git a/test/t/unit/test_unit_xfunc.py b/test/t/unit/test_unit_xfunc.py new file mode 100644 index 0000000..3fb3a72 --- /dev/null +++ b/test/t/unit/test_unit_xfunc.py @@ -0,0 +1,71 @@ +import pytest + +from conftest import assert_bash_exec, bash_env_saved + + +@pytest.mark.bashcomp(cmd=None) +class TestUnitXfunc: + def test_1(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_xfunc", quote=False + ) + + # test precondition + assert_bash_exec( + bash, + "! declare -F _comp_xfunc_xfunc_test1_utility1 &>/dev/null", + ) + + # first invocation (completion/xfunc-test1 is sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test1 utility1 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util1[<a b><cde><fgh>]" + + # test precondition + assert_bash_exec( + bash, "declare -F _comp_xfunc_xfunc_test1_utility1 &>/dev/null" + ) + + # second invocation (completion/xfunc-test1 is not sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test1 utility1 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util1[<a b><cde><fgh>]" + + def test_2(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.write_variable( + "BASH_COMPLETION_USER_DIR", "$PWD/_comp_xfunc", quote=False + ) + + # test precondition + assert_bash_exec( + bash, "! declare -F _comp_xfunc_non_standard_name &>/dev/null" + ) + + # first invocation (completion/xfunc-test2 is sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test2 _comp_xfunc_non_standard_name 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util2[<a b><cde><fgh>]" + + # test precondition + assert_bash_exec( + bash, "declare -F _comp_xfunc_non_standard_name &>/dev/null" + ) + + # second invocation (completion/xfunc-test2 is not sourced) + output = assert_bash_exec( + bash, + "_comp_xfunc xfunc-test2 _comp_xfunc_non_standard_name 'a b' cde fgh", + want_output=True, + ) + assert output.strip() == "util2[<a b><cde><fgh>]" diff --git a/test/t/unit/test_unit_xinetd_services.py b/test/t/unit/test_unit_xinetd_services.py index 7a90cb7..a9e33a9 100644 --- a/test/t/unit/test_unit_xinetd_services.py +++ b/test/t/unit/test_unit_xinetd_services.py @@ -6,17 +6,19 @@ from conftest import assert_bash_exec @pytest.mark.bashcomp(cmd=None, ignore_env=r"^\+COMPREPLY=") class TestUnitXinetdServices: def test_direct(self, bash): - assert_bash_exec(bash, "_xinetd_services >/dev/null") + assert_bash_exec(bash, "_comp_compgen_xinetd_services >/dev/null") def test_env_non_pollution(self, bash): """Test environment non-pollution, detected at teardown.""" - assert_bash_exec(bash, "foo() { _xinetd_services; }; foo; unset foo") + assert_bash_exec( + bash, "foo() { _comp_compgen_xinetd_services; }; foo; unset -f foo" + ) def test_basic(self, bash): output = assert_bash_exec( bash, - "foo() { local BASHCOMP_XINETDDIR=$PWD/shared/bin;unset COMPREPLY; " - '_xinetd_services; printf "%s\\n" "${COMPREPLY[@]}"; }; foo; unset foo', + "foo() { local _comp__test_xinetd_dir=$PWD/shared/bin; unset -v COMPREPLY; " + '_comp_compgen_xinetd_services; printf "%s\\n" "${COMPREPLY[@]}"; }; foo; unset -f foo', want_output=True, ) assert sorted(output.split()) == ["arp", "ifconfig"] |