Skip to content

Commit 936f85a

Browse files
authored
Fix bash completion of command lines containing : (#3205)
1 parent 8fb2740 commit 936f85a

4 files changed

Lines changed: 331 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
2323
camino = "1.0.4"
2424
chrono = "0.4.38"
2525
clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] }
26-
clap_complete = { version = "4.6.0", features = ["unstable-dynamic"] }
26+
clap_complete = { version = "=4.6.0", features = ["unstable-dynamic"] }
2727
clap_mangen = "0.2.20"
2828
derive-where = "1.2.7"
2929
dirs = "6.0.0"

bin/test-bash-completions

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#!/usr/bin/env bash
2+
3+
set -uo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
7+
source "$SCRIPT_DIR/../completion-registration-script.bash"
8+
9+
PASS=0
10+
FAIL=0
11+
12+
assert_reassemble() {
13+
local description="$1"
14+
shift
15+
local comp_line="$1"
16+
shift
17+
local comp_cword="$1"
18+
shift
19+
local comp_wordbreaks="$1"
20+
shift
21+
local expected_index="$1"
22+
shift
23+
local -a expected_words=()
24+
while [[ $# -gt 0 && "$1" != "--" ]]; do
25+
expected_words+=("$1")
26+
shift
27+
done
28+
[[ "${1:-}" == "--" ]] && shift
29+
local -a comp_words_arr=()
30+
while [[ $# -gt 0 ]]; do
31+
comp_words_arr+=("$1")
32+
shift
33+
done
34+
35+
COMP_WORDS=("${comp_words_arr[@]}")
36+
COMP_LINE="$comp_line"
37+
COMP_CWORD="$comp_cword"
38+
COMP_WORDBREAKS="$comp_wordbreaks"
39+
40+
local words=("${COMP_WORDS[@]}")
41+
local _CLAP_COMPLETE_INDEX=${COMP_CWORD}
42+
_clap_reassemble_words
43+
44+
local fail=0
45+
46+
if [[ "$_CLAP_COMPLETE_INDEX" != "$expected_index" ]]; then
47+
echo "FAIL: $description"
48+
echo " _CLAP_COMPLETE_INDEX: expected=$expected_index got=$_CLAP_COMPLETE_INDEX"
49+
fail=1
50+
fi
51+
52+
if [[ "${#words[@]}" != "${#expected_words[@]}" ]]; then
53+
echo "FAIL: $description"
54+
echo " words length: expected=${#expected_words[@]} got=${#words[@]}"
55+
echo " expected: (${expected_words[*]})"
56+
echo " got: (${words[*]})"
57+
fail=1
58+
else
59+
for ((i = 0; i < ${#expected_words[@]}; i++)); do
60+
if [[ "${words[i]}" != "${expected_words[i]}" ]]; then
61+
echo "FAIL: $description"
62+
echo " words[$i]: expected='${expected_words[i]}' got='${words[i]}'"
63+
fail=1
64+
fi
65+
done
66+
fi
67+
68+
if [[ $fail -eq 0 ]]; then
69+
PASS=$((PASS + 1))
70+
else
71+
FAIL=$((FAIL + 1))
72+
fi
73+
}
74+
75+
assert_trim() {
76+
local description="$1"
77+
shift
78+
local comp_wordbreaks="$1"
79+
shift
80+
local clap_index="$1"
81+
shift
82+
local -a expected_reply=()
83+
while [[ $# -gt 0 && "$1" != "--" ]]; do
84+
expected_reply+=("$1")
85+
shift
86+
done
87+
[[ "${1:-}" == "--" ]] && shift
88+
local -a words_arr=()
89+
while [[ $# -gt 0 && "$1" != "--" ]]; do
90+
words_arr+=("$1")
91+
shift
92+
done
93+
[[ "${1:-}" == "--" ]] && shift
94+
COMPREPLY=("$@")
95+
96+
COMP_WORDBREAKS="$comp_wordbreaks"
97+
local words=("${words_arr[@]}")
98+
local _CLAP_COMPLETE_INDEX="$clap_index"
99+
100+
_clap_trim_completions
101+
102+
local fail=0
103+
104+
if [[ "${#COMPREPLY[@]}" != "${#expected_reply[@]}" ]]; then
105+
echo "FAIL: $description"
106+
echo " COMPREPLY length: expected=${#expected_reply[@]} got=${#COMPREPLY[@]}"
107+
echo " expected: (${expected_reply[*]:-})"
108+
echo " got: (${COMPREPLY[*]:-})"
109+
fail=1
110+
else
111+
for ((i = 0; i < ${#expected_reply[@]}; i++)); do
112+
if [[ "${COMPREPLY[i]}" != "${expected_reply[i]}" ]]; then
113+
echo "FAIL: $description"
114+
echo " COMPREPLY[$i]: expected='${expected_reply[i]}' got='${COMPREPLY[i]}'"
115+
fail=1
116+
fi
117+
done
118+
fi
119+
120+
if [[ $fail -eq 0 ]]; then
121+
PASS=$((PASS + 1))
122+
else
123+
FAIL=$((FAIL + 1))
124+
fi
125+
}
126+
127+
# _clap_reassemble_words tests
128+
129+
assert_reassemble \
130+
"no colon in COMP_WORDBREAKS" \
131+
"just foo" 1 " " \
132+
1 "just" "foo" \
133+
-- "just" "foo"
134+
135+
assert_reassemble \
136+
"no colons in input" \
137+
"just foo" 1 " :" \
138+
1 "just" "foo" \
139+
-- "just" "foo"
140+
141+
assert_reassemble \
142+
"single colon" \
143+
"just foo:bar" 3 " :" \
144+
1 "just" "foo:bar" \
145+
-- "just" "foo" ":" "bar"
146+
147+
assert_reassemble \
148+
"double colon" \
149+
"just foo::bar" 4 " :" \
150+
1 "just" "foo::bar" \
151+
-- "just" "foo" ":" ":" "bar"
152+
153+
assert_reassemble \
154+
"trailing colon" \
155+
"just foo:" 2 " :" \
156+
1 "just" "foo:" \
157+
-- "just" "foo" ":"
158+
159+
assert_reassemble \
160+
"trailing double colon" \
161+
"just foo::" 3 " :" \
162+
1 "just" "foo::" \
163+
-- "just" "foo" ":" ":"
164+
165+
assert_reassemble \
166+
"colon with spaces" \
167+
"just foo : bar" 3 " :" \
168+
3 "just" "foo" ":" "bar" \
169+
-- "just" "foo" ":" "bar"
170+
171+
assert_reassemble \
172+
"multiple colon-separated args" \
173+
"just a:b c:d" 6 " :" \
174+
2 "just" "a:b" "c:d" \
175+
-- "just" "a" ":" "b" "c" ":" "d"
176+
177+
assert_reassemble \
178+
"cursor on colon" \
179+
"just foo:" 2 " :" \
180+
1 "just" "foo:" \
181+
-- "just" "foo" ":"
182+
183+
assert_reassemble \
184+
"cursor on word after colon" \
185+
"just foo:bar" 3 " :" \
186+
1 "just" "foo:bar" \
187+
-- "just" "foo" ":" "bar"
188+
189+
assert_reassemble \
190+
"cursor on word before colon" \
191+
"just foo:bar" 1 " :" \
192+
1 "just" "foo:bar" \
193+
-- "just" "foo" ":" "bar"
194+
195+
assert_reassemble \
196+
"just the command" \
197+
"just" 0 " :" \
198+
0 "just" \
199+
-- "just"
200+
201+
assert_reassemble \
202+
"leading colon word" \
203+
"just :foo" 2 " :" \
204+
1 "just" ":foo" \
205+
-- "just" ":" "foo"
206+
207+
# _clap_trim_completions tests
208+
209+
assert_trim \
210+
"no colon in current word" \
211+
" :" 1 \
212+
"foo:baz" "foo:qux" \
213+
-- "just" "foo" \
214+
-- "foo:baz" "foo:qux"
215+
216+
assert_trim \
217+
"no colon in COMP_WORDBREAKS" \
218+
" " 1 \
219+
"foo:baz" "foo:qux" \
220+
-- "just" "foo:bar" \
221+
-- "foo:baz" "foo:qux"
222+
223+
assert_trim \
224+
"single colon prefix" \
225+
" :" 1 \
226+
"baz" "qux" \
227+
-- "just" "foo:bar" \
228+
-- "foo:baz" "foo:qux"
229+
230+
assert_trim \
231+
"double colon prefix" \
232+
" :" 1 \
233+
"baz" "qux" \
234+
-- "just" "foo::bar" \
235+
-- "foo::baz" "foo::qux"
236+
237+
assert_trim \
238+
"empty COMPREPLY" \
239+
" :" 1 \
240+
-- "just" "foo:bar" \
241+
--
242+
243+
assert_trim \
244+
"mixed prefixes" \
245+
" :" 1 \
246+
"baz" "other" \
247+
-- "just" "foo:bar" \
248+
-- "foo:baz" "other"
249+
250+
# summary
251+
252+
echo ""
253+
echo "PASS: $PASS"
254+
echo "FAIL: $FAIL"
255+
256+
if [[ $FAIL -gt 0 ]]; then
257+
exit 1
258+
fi
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
_clap_reassemble_words() {
2+
if [[ "$COMP_WORDBREAKS" != *:* ]]; then
3+
return
4+
fi
5+
local i j=0 line=$COMP_LINE
6+
words=()
7+
_CLAP_COMPLETE_INDEX=0
8+
for ((i = 0; i < ${#COMP_WORDS[@]}; i++)); do
9+
if ((i > 0 && j > 0)) && [[ "${COMP_WORDS[i]}" == :* || "${words[j-1]}" == *: ]] && [[ "$line" != [[:blank:]]* ]]; then
10+
words[j-1]="${words[j-1]}${COMP_WORDS[i]}"
11+
else
12+
words[j]="${COMP_WORDS[i]}"
13+
((j++))
14+
fi
15+
if ((i == COMP_CWORD)); then
16+
_CLAP_COMPLETE_INDEX=$((j - 1))
17+
fi
18+
line=${line#*"${COMP_WORDS[i]}"}
19+
done
20+
}
21+
22+
_clap_trim_completions() {
23+
local cur="${words[_CLAP_COMPLETE_INDEX]}"
24+
if [[ "$cur" != *:* || "$COMP_WORDBREAKS" != *:* ]]; then
25+
return
26+
fi
27+
local colon_word=${cur%"${cur##*:}"}
28+
local i=${#COMPREPLY[*]}
29+
while [[ $((--i)) -ge 0 ]]; do
30+
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
31+
done
32+
}
33+
34+
_clap_complete_just() {
35+
local IFS=$'\013'
36+
local _CLAP_COMPLETE_INDEX=${COMP_CWORD}
37+
local _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE}
38+
if compopt +o nospace 2> /dev/null; then
39+
local _CLAP_COMPLETE_SPACE=false
40+
else
41+
local _CLAP_COMPLETE_SPACE=true
42+
fi
43+
local words=("${COMP_WORDS[@]}")
44+
_clap_reassemble_words
45+
COMPREPLY=( $( \
46+
_CLAP_IFS="$IFS" \
47+
_CLAP_COMPLETE_INDEX="$_CLAP_COMPLETE_INDEX" \
48+
_CLAP_COMPLETE_COMP_TYPE="$_CLAP_COMPLETE_COMP_TYPE" \
49+
_CLAP_COMPLETE_SPACE="$_CLAP_COMPLETE_SPACE" \
50+
JUST_COMPLETE="bash" \
51+
just -- "${words[@]}" \
52+
) )
53+
if [[ $? != 0 ]]; then
54+
unset COMPREPLY
55+
elif [[ $_CLAP_COMPLETE_SPACE == false ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
56+
compopt -o nospace
57+
fi
58+
_clap_trim_completions
59+
}
60+
61+
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
62+
complete -o nospace -o bashdefault -o nosort -F _clap_complete_just just
63+
else
64+
complete -o nospace -o bashdefault -F _clap_complete_just just
65+
fi

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ use {
66
};
77

88
fn main() {
9+
if env::var_os("JUST_COMPLETE").is_some_and(|value| value == "bash")
10+
&& env::args_os().nth(1).is_none()
11+
{
12+
print!("{}", include_str!("../completion-registration-script.bash"));
13+
return;
14+
}
15+
916
CompleteEnv::with_factory(Arguments::command)
1017
.var("JUST_COMPLETE")
1118
.complete();

0 commit comments

Comments
 (0)