Skip to content

Commit 33bce2b

Browse files
committed
Bash completion v2
This v2 version of bash completion is based on Go completions. It also supports descriptions like fish and zsh. Signed-off-by: Marc Khouzam <[email protected]>
1 parent c6fe2d4 commit 33bce2b

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed

bash_completionsV2.go

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package cobra
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"os"
8+
)
9+
10+
func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error {
11+
buf := new(bytes.Buffer)
12+
genBashComp(buf, c.Name(), includeDesc)
13+
_, err := buf.WriteTo(w)
14+
return err
15+
}
16+
17+
func genBashComp(buf *bytes.Buffer, name string, includeDesc bool) {
18+
compCmd := ShellCompRequestCmd
19+
if !includeDesc {
20+
compCmd = ShellCompNoDescRequestCmd
21+
}
22+
23+
buf.WriteString(fmt.Sprintf(`# bash completion for %-36[1]s -*- shell-script -*-
24+
25+
__%[1]s_debug()
26+
{
27+
if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then
28+
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
29+
fi
30+
}
31+
32+
__%[1]s_perform_completion()
33+
{
34+
__%[1]s_debug
35+
__%[1]s_debug "========= starting completion logic =========="
36+
__%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
37+
38+
# The user could have moved the cursor backwards on the command-line.
39+
# We need to trigger completion from the $cword location, so we need
40+
# to truncate the command-line ($words) up to the $cword location.
41+
words=(${words[*]:0:$cword+1})
42+
__%[1]s_debug "Truncated words[*]: ${words[*]},"
43+
44+
local shellCompDirectiveError=%[3]d
45+
local shellCompDirectiveNoSpace=%[4]d
46+
local shellCompDirectiveNoFileComp=%[5]d
47+
local shellCompDirectiveFilterFileExt=%[6]d
48+
local shellCompDirectiveFilterDirs=%[7]d
49+
50+
local out requestComp lastParam lastChar comp directive args flagPrefix
51+
52+
# Prepare the command to request completions for the program.
53+
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases
54+
args=("${words[@]:1}")
55+
requestComp="${words[0]} %[2]s ${args[*]}"
56+
57+
lastParam=${words[$((${#words[@]}-1))]}
58+
lastChar=${lastParam:$((${#lastParam}-1)):1}
59+
__%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}"
60+
61+
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
62+
# If the last parameter is complete (there is a space following it)
63+
# We add an extra empty parameter so we can indicate this to the go method.
64+
__%[1]s_debug "Adding extra empty parameter"
65+
requestComp="${requestComp} \"\""
66+
fi
67+
68+
# When completing a flag with an = (e.g., %[1]s -n=<TAB>)
69+
# bash focuses on the part after the =, so we need to remove
70+
# the flag part from $cur
71+
if [[ "${cur}" == -*=* ]]; then
72+
flagPrefix="${cur%%%%=*}="
73+
cur="${cur#*=}"
74+
fi
75+
76+
__%[1]s_debug "Calling ${requestComp}"
77+
# Use eval to handle any environment variables and such
78+
out=$(eval "${requestComp}" 2>/dev/null)
79+
80+
# Extract the directive integer at the very end of the output following a colon (:)
81+
directive=${out##*:}
82+
# Remove the directive
83+
out=${out%%:*}
84+
if [ "${directive}" = "${out}" ]; then
85+
# There is not directive specified
86+
directive=0
87+
fi
88+
__%[1]s_debug "The completion directive is: ${directive}"
89+
__%[1]s_debug "The completions are: ${out[*]}"
90+
91+
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
92+
# Error code. No completion.
93+
__%[1]s_debug "Received error from custom completion go code"
94+
return
95+
else
96+
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
97+
if [[ $(type -t compopt) = "builtin" ]]; then
98+
__%[1]s_debug "Activating no space"
99+
compopt -o nospace
100+
fi
101+
fi
102+
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
103+
if [[ $(type -t compopt) = "builtin" ]]; then
104+
__%[1]s_debug "Activating no file completion"
105+
compopt +o default
106+
fi
107+
fi
108+
fi
109+
110+
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
111+
# File extension filtering
112+
local fullFilter filter filteringCmd
113+
114+
# Do not use quotes around the $out variable or else newline
115+
# characters will be kept.
116+
for filter in ${out[*]}; do
117+
fullFilter+="$filter|"
118+
done
119+
120+
filteringCmd="_filedir $fullFilter"
121+
__%[1]s_debug "File filtering command: $filteringCmd"
122+
$filteringCmd
123+
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
124+
# File completion for directories only
125+
126+
# Use printf to strip any trailing newline
127+
local subdir=$(printf "%%s" "${out[0]}")
128+
if [ -n "$subdir" ]; then
129+
__%[1]s_debug "Listing directories in $subdir"
130+
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
131+
else
132+
__%[1]s_debug "Listing directories in ."
133+
_filedir -d
134+
fi
135+
else
136+
local tab=$(printf '\t')
137+
local longest=0
138+
# Look for the longest completion so that we can format things nicely
139+
while IFS='' read -r comp; do
140+
comp=${comp%%%%$tab*}
141+
if ((${#comp}>$longest)); then
142+
longest=${#comp}
143+
fi
144+
done < <(printf "%%s\n" "${out[@]}")
145+
146+
local completions=()
147+
while IFS='' read -r comp; do
148+
if [ -z "$comp" ]; then
149+
continue
150+
fi
151+
152+
__%[1]s_debug "Original comp: $comp"
153+
comp="$(__%[1]s_format_comp_descriptions "$comp" $longest)"
154+
__%[1]s_debug "Final comp: $comp"
155+
completions+=("$comp")
156+
done < <(printf "%%s\n" "${out[@]}")
157+
158+
while IFS='' read -r comp; do
159+
# Although this script should only be used for bash
160+
# there may be programs that still convert the bash
161+
# script into a zsh one. To continue supporting those
162+
# programs, we do this single adaptation for zsh
163+
if [ -n "${ZSH_VERSION}" ]; then
164+
# zsh completion needs --flag= prefix
165+
COMPREPLY+=("$flagPrefix$comp")
166+
else
167+
COMPREPLY+=("$comp")
168+
fi
169+
done < <(compgen -W "${completions[*]}" -- "$cur")
170+
171+
# If there is a single completion left, remove the description text
172+
if [ ${#COMPREPLY[*]} -eq 1 ]; then
173+
__%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
174+
comp="${COMPREPLY[0]%%%% *}"
175+
__%[1]s_debug "Removed description from single completion, which is now: ${comp}"
176+
COMPREPLY=()
177+
COMPREPLY+=("$comp")
178+
fi
179+
fi
180+
181+
__%[1]s_handle_special_char "$cur" :
182+
__%[1]s_handle_special_char "$cur" =
183+
}
184+
185+
__%[1]s_handle_special_char()
186+
{
187+
local comp="$1"
188+
local char=$2
189+
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
190+
local word=${comp%%"${comp##*${char}}"}
191+
local idx=${#COMPREPLY[*]}
192+
while [[ $((--idx)) -ge 0 ]]; do
193+
COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"}
194+
done
195+
fi
196+
}
197+
198+
__%[1]s_format_comp_descriptions()
199+
{
200+
local tab=$(printf '\t')
201+
local comp="$1"
202+
local longest=$2
203+
204+
# Properly format the description string which follows a tab character if there is one
205+
if [[ "$comp" == *$tab* ]]; then
206+
desc=${comp#*$tab}
207+
comp=${comp%%%%$tab*}
208+
209+
# $COLUMNS stores the current shell width.
210+
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
211+
maxdesclength=$(( $COLUMNS - $longest - 4 ))
212+
213+
# Make sure we can fit a description of at least 8 characters
214+
# if we are to align the descriptions.
215+
if [[ $maxdesclength -gt 8 ]]; then
216+
# Add the proper number of spaces to align the descriptions
217+
for ((i = ${#comp} ; i < $longest ; i++)); do
218+
comp+=" "
219+
done
220+
else
221+
# Don't pad the descriptions so we can fit more text after the completion
222+
maxdesclength=$(( $COLUMNS - ${#comp} - 4 ))
223+
fi
224+
225+
# If there is enough space for any description text,
226+
# truncate the descriptions that are too long for the shell width
227+
if [ $maxdesclength -gt 0 ]; then
228+
if [ ${#desc} -gt $maxdesclength ]; then
229+
desc=${desc:0:$(( $maxdesclength - 1 ))}
230+
desc+="…"
231+
fi
232+
comp+=" ($desc)"
233+
234+
# Now escape all spaces
235+
comp=${comp// /\\ }
236+
# Escape single quotes
237+
comp=${comp//\'/\\\'}
238+
# Escape backticks
239+
comp=${comp//\%[8]c/\\\%[8]c}
240+
fi
241+
fi
242+
243+
echo "$comp"
244+
}
245+
246+
__start_%[1]s()
247+
{
248+
local cur prev words cword
249+
250+
COMPREPLY=()
251+
_get_comp_words_by_ref -n "=:" cur prev words cword
252+
253+
__%[1]s_perform_completion
254+
}
255+
256+
if [[ $(type -t compopt) = "builtin" ]]; then
257+
complete -o default -F __start_%[1]s %[1]s
258+
else
259+
complete -o default -o nospace -F __start_%[1]s %[1]s
260+
fi
261+
262+
# ex: ts=4 sw=4 et filetype=sh
263+
`, name, compCmd,
264+
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
265+
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, '`'))
266+
}
267+
268+
// GenBashCompletionFileV2 generates Bash completion version 2.
269+
func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error {
270+
outFile, err := os.Create(filename)
271+
if err != nil {
272+
return err
273+
}
274+
defer outFile.Close()
275+
276+
return c.GenBashCompletionV2(outFile, includeDesc)
277+
}
278+
279+
// GenBashCompletionV2 generates Bash completion file version 2
280+
// and writes it to the passed writer.
281+
func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error {
282+
return c.genBashCompletion(w, includeDesc)
283+
}

0 commit comments

Comments
 (0)