diff --git a/bash_completion b/bash_completion index b13becc2e71..d36fbb7d7c1 100644 --- a/bash_completion +++ b/bash_completion @@ -2765,28 +2765,71 @@ _comp_command_offset() # *something* for every command thrown at it ($cspec != empty) _comp_complete_minimal "${args[@]}" fi - elif [[ $cspec == *' -F '* ]]; then - # complete -F - - # get function name - local func=${cspec#* -F } - func=${func%% *} - $func "${args[@]}" - - # restart completion (once) if function exited with 124 - if (($? == 124 && retry_count++ == 0)); then - # Note: When the completion function returns 124, the state - # of COMPREPLY is discarded. - COMPREPLY=() + elif [[ $cspec == *\ -[CF]\ * ]]; then + if [[ $cspec == *' -F '* ]]; then + # complete -F + + # get function name + local func=${cspec#* -F } + func=${func%% *} + $func "${args[@]}" + + # restart completion (once) if function exited with 124 + if (($? == 124 && retry_count++ == 0)); then + # Note: When the completion function returns 124, the + # state of COMPREPLY is discarded. + COMPREPLY=() + + cspec=$(complete -p "$compcmd" 2>/dev/null) + + # Note: When completion spec is removed after 124, we + # do not generate any completions including the default + # ones. This is the behavior of the original Bash + # progcomp. + [[ $cspec ]] || break + + continue + fi + else + # complete -C - cspec=$(complete -p "$compcmd" 2>/dev/null) + # get command name + local completer=${cspec#* -C \'} - # Note: When completion spec is removed after 124, we do - # not generate any completions including the default ones. - # This is the behavior of the original Bash progcomp. - [[ $cspec ]] || break + # completer commands are always single-quoted + if ! _comp_dequote "'$completer"; then + _minimal "${args[@]}" + break + fi + completer=${REPLY[0]} + + local -a suggestions + + local IFS=$' \t\n' + local reset_monitor=$(shopt -po monitor) reset_lastpipe=$(shopt -p lastpipe) reset_noglob=$(shopt -po noglob) + set +o monitor + shopt -s lastpipe + set -o noglob + + COMP_KEY="$COMP_KEY" COMP_LINE="$COMP_LINE" \ + COMP_POINT="$COMP_POINT" COMP_TYPE="$COMP_TYPE" \ + $completer "${args[@]}" | mapfile -t suggestions - continue + $reset_monitor + $reset_lastpipe + $reset_noglob + _comp_unlocal IFS + + local suggestion + local i=0 + COMPREPLY=() + for suggestion in "${suggestions[@]}"; do + COMPREPLY[i]+=${COMPREPLY[i]+$'\n'}$suggestion + + if [[ $suggestion != *\\ ]]; then + ((i++)) + fi + done fi # restore initial compopts diff --git a/test/fixtures/_command_offset/completer b/test/fixtures/_command_offset/completer new file mode 100755 index 00000000000..8253e6d2ce2 --- /dev/null +++ b/test/fixtures/_command_offset/completer @@ -0,0 +1,30 @@ +#!/bin/sh + +case "${2-}" in + b|ba|bar) + echo bar + ;; + cont1*) + echo cont10 + echo cont11\\ + ;; + f|fo|foo) + echo foo + ;; + l) + echo line\\ + echo two + echo long + ;; + li*) + echo line\\ + echo two + ;; + lo*) + echo long + ;; + *) + echo bar + echo foo + ;; +esac diff --git a/test/t/unit/test_unit_command_offset.py b/test/t/unit/test_unit_command_offset.py index 94636dd7f8b..0e32c1f8076 100644 --- a/test/t/unit/test_unit_command_offset.py +++ b/test/t/unit/test_unit_command_offset.py @@ -12,7 +12,8 @@ def join(words): @pytest.mark.bashcomp( cmd=None, - ignore_env=r"^[+-]COMPREPLY=", + cwd="_command_offset", + ignore_env=r"^[+-](COMPREPLY|REPLY)=", ) class TestUnitCommandOffset: wordlist = sorted(["foo", "bar"]) @@ -45,6 +46,8 @@ def functions(self, bash): ) 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) @@ -78,6 +81,21 @@ def test_2(self, bash, functions, cmd, expected_completion): """ 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