Skip to content

Commit 3a615b0

Browse files
committed
fix(_comp_command_offset): Add support for complete -C
1 parent 5052702 commit 3a615b0

File tree

3 files changed

+119
-21
lines changed

3 files changed

+119
-21
lines changed

bash_completion

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,33 +2307,83 @@ _comp_command_offset()
23072307
# *something* for every command thrown at it ($cspec != empty)
23082308
_minimal
23092309
fi
2310-
elif [[ $cspec == *' -F '* ]]; then
2311-
# complete -F <function>
2310+
elif [[ $cspec == *\ -[CF]\ * ]]; then
2311+
if [[ $cspec == *' -F '* ]]; then
2312+
# complete -F <function>
23122313

2313-
# get function name
2314-
local func=${cspec#* -F }
2315-
func=${func%% *}
2314+
# get function name
2315+
local func=${cspec#* -F }
2316+
func=${func%% *}
23162317

2317-
if ((${#COMP_WORDS[@]} >= 2)); then
2318-
$func "$cmd" "${COMP_WORDS[-1]}" "${COMP_WORDS[-2]}"
2318+
if ((${#COMP_WORDS[@]} >= 2)); then
2319+
$func "$cmd" "${COMP_WORDS[-1]}" "${COMP_WORDS[-2]}"
2320+
else
2321+
$func "$cmd" "${COMP_WORDS[-1]}"
2322+
fi
2323+
2324+
# restart completion (once) if function exited with 124
2325+
if (($? == 124 && retry_count++ == 0)); then
2326+
# Note: When the completion function returns 124, the
2327+
# state of COMPREPLY is discarded.
2328+
COMPREPLY=()
2329+
2330+
cspec=$(complete -p "$compcmd" 2>/dev/null)
2331+
2332+
# Note: When completion spec is removed after 124, we
2333+
# do not generate any completions including the default
2334+
# ones. This is the behavior of the original Bash
2335+
# progcomp.
2336+
[[ $cspec ]] || break
2337+
2338+
continue
2339+
fi
23192340
else
2320-
$func "$cmd" "${COMP_WORDS[-1]}"
2321-
fi
2341+
# complete -C <command>
23222342

2323-
# restart completion (once) if function exited with 124
2324-
if (($? == 124 && retry_count++ == 0)); then
2325-
# Note: When the completion function returns 124, the state
2326-
# of COMPREPLY is discarded.
2327-
COMPREPLY=()
2343+
# get command name
2344+
local completer=${cspec#* -C \'}
23282345

2329-
cspec=$(complete -p "$compcmd" 2>/dev/null)
2346+
# completer commands are always single-quoted
2347+
if ! _comp_dequote "'$completer"; then
2348+
_minimal
2349+
break
2350+
fi
2351+
completer=${ret[0]}
2352+
2353+
local -a suggestions
2354+
2355+
local reset_monitor=$(shopt -po monitor) reset_lastpipe=$(shopt -p lastpipe)
2356+
set +o monitor
2357+
shopt -s lastpipe
2358+
2359+
(
2360+
export COMP_KEY COMP_LINE COMP_POINT COMP_TYPE
23302361

2331-
# Note: When completion spec is removed after 124, we do
2332-
# not generate any completions including the default ones.
2333-
# This is the behavior of the original Bash progcomp.
2334-
[[ $cspec ]] || break
2362+
if ((${#COMP_WORDS[@]} >= 2)); then
2363+
$completer "$cmd" "${COMP_WORDS[-1]}" \
2364+
"${COMP_WORDS[-2]}"
2365+
else
2366+
$completer "$cmd" "${COMP_WORDS[-1]}"
2367+
fi
2368+
) | mapfile suggestions
23352369

2336-
continue
2370+
$reset_monitor
2371+
$reset_lastpipe
2372+
2373+
local suggestion
2374+
local i=0
2375+
COMPREPLY=()
2376+
for suggestion in "${suggestions[@]}"; do
2377+
# `mapfile -t` was added in bash 4.4, so we have to
2378+
# strip trailing newlines manually
2379+
suggestion="${suggestion%$'\n'}"
2380+
2381+
COMPREPLY[i]+=${COMPREPLY[i]+$'\n'}$suggestion
2382+
2383+
if [[ $suggestion != *\\ ]]; then
2384+
((i++))
2385+
fi
2386+
done
23372387
fi
23382388

23392389
# restore initial compopts
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/sh
2+
3+
case "${2-}" in
4+
b|ba|bar)
5+
echo bar
6+
;;
7+
cont1*)
8+
echo cont10
9+
echo cont11\\
10+
;;
11+
f|fo|foo)
12+
echo foo
13+
;;
14+
l)
15+
echo line\\
16+
echo two
17+
echo long
18+
;;
19+
li*)
20+
echo line\\
21+
echo two
22+
;;
23+
lo*)
24+
echo long
25+
;;
26+
*)
27+
echo bar
28+
echo foo
29+
;;
30+
esac

test/t/unit/test_unit_command_offset.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ def join(words):
1212

1313
@pytest.mark.bashcomp(
1414
cmd=None,
15-
ignore_env=r"^[+-]COMPREPLY=",
15+
cwd="_command_offset",
16+
ignore_env=r"^[+-](COMPREPLY|ret)=",
1617
)
1718
class TestUnitCommandOffset:
1819
wordlist = sorted(["foo", "bar"])
@@ -40,6 +41,8 @@ def functions(self, bash):
4041
% {"idx": idx, "comp": comp},
4142
)
4243

44+
assert_bash_exec(bash, "complete -C ./completer cmd6")
45+
4346
def test_1(self, bash, functions):
4447
assert_complete(bash, 'cmd1 "/tmp/aaa bbb" ')
4548
assert_bash_exec(bash, "! complete -p aaa", want_output=None)
@@ -72,3 +75,18 @@ def test_2(self, bash, functions, cmd, expected_completion):
7275
cleared before the retry.
7376
"""
7477
assert assert_complete(bash, "meta %s " % cmd) == expected_completion
78+
79+
@pytest.mark.parametrize(
80+
"cmd,expected_completion",
81+
[
82+
("cmd6 ", wordlist),
83+
("cmd6 l", ["line\\^Jtwo", "long"]),
84+
("cmd6 lo", ["ng"]),
85+
("cmd6 line", ["\\^Jtwo"]),
86+
("cmd6 cont1", ["cont10", "cont11\\"]),
87+
],
88+
)
89+
def test_3(self, bash, functions, cmd, expected_completion):
90+
got = assert_complete(bash, f"cmd1 {cmd}")
91+
assert got == assert_complete(bash, cmd)
92+
assert got == expected_completion

0 commit comments

Comments
 (0)