256
|
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 io.StringWriter, name string, includeDesc bool) { |
|
18 |
compCmd := ShellCompRequestCmd |
|
19 |
if !includeDesc { |
|
20 |
compCmd = ShellCompNoDescRequestCmd |
|
21 |
} |
|
22 |
|
|
23 |
WriteStringAndCheck(buf, fmt.Sprintf(`# bash completion V2 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 |
# Macs have bash3 for which the bash-completion package doesn't include |
|
33 |
# _init_completion. This is a minimal version of that function. |
|
34 |
__%[1]s_init_completion() |
|
35 |
{ |
|
36 |
COMPREPLY=() |
|
37 |
_get_comp_words_by_ref "$@" cur prev words cword |
|
38 |
} |
|
39 |
|
|
40 |
# This function calls the %[1]s program to obtain the completion |
|
41 |
# results and the directive. It fills the 'out' and 'directive' vars. |
|
42 |
__%[1]s_get_completion_results() { |
|
43 |
local requestComp lastParam lastChar args |
|
44 |
|
|
45 |
# Prepare the command to request completions for the program. |
|
46 |
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases |
|
47 |
args=("${words[@]:1}") |
|
48 |
requestComp="${words[0]} %[2]s ${args[*]}" |
|
49 |
|
|
50 |
lastParam=${words[$((${#words[@]}-1))]} |
|
51 |
lastChar=${lastParam:$((${#lastParam}-1)):1} |
|
52 |
__%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" |
|
53 |
|
|
54 |
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then |
|
55 |
# If the last parameter is complete (there is a space following it) |
|
56 |
# We add an extra empty parameter so we can indicate this to the go method. |
|
57 |
__%[1]s_debug "Adding extra empty parameter" |
|
58 |
requestComp="${requestComp} ''" |
|
59 |
fi |
|
60 |
|
|
61 |
# When completing a flag with an = (e.g., %[1]s -n=<TAB>) |
|
62 |
# bash focuses on the part after the =, so we need to remove |
|
63 |
# the flag part from $cur |
|
64 |
if [[ "${cur}" == -*=* ]]; then |
|
65 |
cur="${cur#*=}" |
|
66 |
fi |
|
67 |
|
|
68 |
__%[1]s_debug "Calling ${requestComp}" |
|
69 |
# Use eval to handle any environment variables and such |
|
70 |
out=$(eval "${requestComp}" 2>/dev/null) |
|
71 |
|
|
72 |
# Extract the directive integer at the very end of the output following a colon (:) |
|
73 |
directive=${out##*:} |
|
74 |
# Remove the directive |
|
75 |
out=${out%%:*} |
|
76 |
if [ "${directive}" = "${out}" ]; then |
|
77 |
# There is not directive specified |
|
78 |
directive=0 |
|
79 |
fi |
|
80 |
__%[1]s_debug "The completion directive is: ${directive}" |
260
|
81 |
__%[1]s_debug "The completions are: ${out}" |
256
|
82 |
} |
|
83 |
|
|
84 |
__%[1]s_process_completion_results() { |
|
85 |
local shellCompDirectiveError=%[3]d |
|
86 |
local shellCompDirectiveNoSpace=%[4]d |
|
87 |
local shellCompDirectiveNoFileComp=%[5]d |
|
88 |
local shellCompDirectiveFilterFileExt=%[6]d |
|
89 |
local shellCompDirectiveFilterDirs=%[7]d |
|
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 |
else |
|
101 |
__%[1]s_debug "No space directive not supported in this version of bash" |
|
102 |
fi |
|
103 |
fi |
|
104 |
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then |
|
105 |
if [[ $(type -t compopt) = "builtin" ]]; then |
|
106 |
__%[1]s_debug "Activating no file completion" |
|
107 |
compopt +o default |
|
108 |
else |
|
109 |
__%[1]s_debug "No file completion directive not supported in this version of bash" |
|
110 |
fi |
|
111 |
fi |
|
112 |
fi |
|
113 |
|
260
|
114 |
# Separate activeHelp from normal completions |
|
115 |
local completions=() |
|
116 |
local activeHelp=() |
|
117 |
__%[1]s_extract_activeHelp |
|
118 |
|
256
|
119 |
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then |
|
120 |
# File extension filtering |
|
121 |
local fullFilter filter filteringCmd |
|
122 |
|
260
|
123 |
# Do not use quotes around the $completions variable or else newline |
256
|
124 |
# characters will be kept. |
260
|
125 |
for filter in ${completions[*]}; do |
256
|
126 |
fullFilter+="$filter|" |
|
127 |
done |
|
128 |
|
|
129 |
filteringCmd="_filedir $fullFilter" |
|
130 |
__%[1]s_debug "File filtering command: $filteringCmd" |
|
131 |
$filteringCmd |
|
132 |
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then |
|
133 |
# File completion for directories only |
|
134 |
|
|
135 |
# Use printf to strip any trailing newline |
|
136 |
local subdir |
260
|
137 |
subdir=$(printf "%%s" "${completions[0]}") |
256
|
138 |
if [ -n "$subdir" ]; then |
|
139 |
__%[1]s_debug "Listing directories in $subdir" |
|
140 |
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return |
|
141 |
else |
|
142 |
__%[1]s_debug "Listing directories in ." |
|
143 |
_filedir -d |
|
144 |
fi |
|
145 |
else |
260
|
146 |
__%[1]s_handle_completion_types |
256
|
147 |
fi |
|
148 |
|
|
149 |
__%[1]s_handle_special_char "$cur" : |
|
150 |
__%[1]s_handle_special_char "$cur" = |
260
|
151 |
|
|
152 |
# Print the activeHelp statements before we finish |
|
153 |
if [ ${#activeHelp} -ne 0 ]; then |
|
154 |
printf "\n"; |
|
155 |
printf "%%s\n" "${activeHelp[@]}" |
|
156 |
printf "\n" |
|
157 |
|
|
158 |
# The prompt format is only available from bash 4.4. |
|
159 |
# We test if it is available before using it. |
|
160 |
if (x=${PS1@P}) 2> /dev/null; then |
|
161 |
printf "%%s" "${PS1@P}${COMP_LINE[@]}" |
|
162 |
else |
|
163 |
# Can't print the prompt. Just print the |
|
164 |
# text the user had typed, it is workable enough. |
|
165 |
printf "%%s" "${COMP_LINE[@]}" |
|
166 |
fi |
|
167 |
fi |
|
168 |
} |
|
169 |
|
|
170 |
# Separate activeHelp lines from real completions. |
|
171 |
# Fills the $activeHelp and $completions arrays. |
|
172 |
__%[1]s_extract_activeHelp() { |
|
173 |
local activeHelpMarker="%[8]s" |
|
174 |
local endIndex=${#activeHelpMarker} |
|
175 |
|
|
176 |
while IFS='' read -r comp; do |
|
177 |
if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then |
|
178 |
comp=${comp:endIndex} |
|
179 |
__%[1]s_debug "ActiveHelp found: $comp" |
|
180 |
if [ -n "$comp" ]; then |
|
181 |
activeHelp+=("$comp") |
|
182 |
fi |
|
183 |
else |
|
184 |
# Not an activeHelp line but a normal completion |
|
185 |
completions+=("$comp") |
|
186 |
fi |
|
187 |
done < <(printf "%%s\n" "${out}") |
|
188 |
} |
|
189 |
|
|
190 |
__%[1]s_handle_completion_types() { |
|
191 |
__%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE" |
|
192 |
|
|
193 |
case $COMP_TYPE in |
|
194 |
37|42) |
|
195 |
# Type: menu-complete/menu-complete-backward and insert-completions |
|
196 |
# If the user requested inserting one completion at a time, or all |
|
197 |
# completions at once on the command-line we must remove the descriptions. |
|
198 |
# https://github.com/spf13/cobra/issues/1508 |
|
199 |
local tab=$'\t' comp |
|
200 |
while IFS='' read -r comp; do |
|
201 |
[[ -z $comp ]] && continue |
|
202 |
# Strip any description |
|
203 |
comp=${comp%%%%$tab*} |
|
204 |
# Only consider the completions that match |
|
205 |
if [[ $comp == "$cur"* ]]; then |
|
206 |
COMPREPLY+=("$comp") |
|
207 |
fi |
|
208 |
done < <(printf "%%s\n" "${completions[@]}") |
|
209 |
;; |
|
210 |
|
|
211 |
*) |
|
212 |
# Type: complete (normal completion) |
|
213 |
__%[1]s_handle_standard_completion_case |
|
214 |
;; |
|
215 |
esac |
256
|
216 |
} |
|
217 |
|
|
218 |
__%[1]s_handle_standard_completion_case() { |
260
|
219 |
local tab=$'\t' comp |
|
220 |
|
|
221 |
# Short circuit to optimize if we don't have descriptions |
|
222 |
if [[ "${completions[*]}" != *$tab* ]]; then |
|
223 |
IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") |
|
224 |
return 0 |
|
225 |
fi |
256
|
226 |
|
|
227 |
local longest=0 |
260
|
228 |
local compline |
256
|
229 |
# Look for the longest completion so that we can format things nicely |
260
|
230 |
while IFS='' read -r compline; do |
|
231 |
[[ -z $compline ]] && continue |
256
|
232 |
# Strip any description before checking the length |
260
|
233 |
comp=${compline%%%%$tab*} |
256
|
234 |
# Only consider the completions that match |
260
|
235 |
[[ $comp == "$cur"* ]] || continue |
|
236 |
COMPREPLY+=("$compline") |
256
|
237 |
if ((${#comp}>longest)); then |
|
238 |
longest=${#comp} |
|
239 |
fi |
260
|
240 |
done < <(printf "%%s\n" "${completions[@]}") |
256
|
241 |
|
|
242 |
# If there is a single completion left, remove the description text |
|
243 |
if [ ${#COMPREPLY[*]} -eq 1 ]; then |
|
244 |
__%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" |
260
|
245 |
comp="${COMPREPLY[0]%%%%$tab*}" |
256
|
246 |
__%[1]s_debug "Removed description from single completion, which is now: ${comp}" |
260
|
247 |
COMPREPLY[0]=$comp |
|
248 |
else # Format the descriptions |
|
249 |
__%[1]s_format_comp_descriptions $longest |
256
|
250 |
fi |
|
251 |
} |
|
252 |
|
|
253 |
__%[1]s_handle_special_char() |
|
254 |
{ |
|
255 |
local comp="$1" |
|
256 |
local char=$2 |
|
257 |
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then |
|
258 |
local word=${comp%%"${comp##*${char}}"} |
|
259 |
local idx=${#COMPREPLY[*]} |
|
260 |
while [[ $((--idx)) -ge 0 ]]; do |
|
261 |
COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} |
|
262 |
done |
|
263 |
fi |
|
264 |
} |
|
265 |
|
|
266 |
__%[1]s_format_comp_descriptions() |
|
267 |
{ |
260
|
268 |
local tab=$'\t' |
|
269 |
local comp desc maxdesclength |
|
270 |
local longest=$1 |
256
|
271 |
|
260
|
272 |
local i ci |
|
273 |
for ci in ${!COMPREPLY[*]}; do |
|
274 |
comp=${COMPREPLY[ci]} |
|
275 |
# Properly format the description string which follows a tab character if there is one |
|
276 |
if [[ "$comp" == *$tab* ]]; then |
|
277 |
__%[1]s_debug "Original comp: $comp" |
|
278 |
desc=${comp#*$tab} |
|
279 |
comp=${comp%%%%$tab*} |
256
|
280 |
|
260
|
281 |
# $COLUMNS stores the current shell width. |
|
282 |
# Remove an extra 4 because we add 2 spaces and 2 parentheses. |
|
283 |
maxdesclength=$(( COLUMNS - longest - 4 )) |
256
|
284 |
|
260
|
285 |
# Make sure we can fit a description of at least 8 characters |
|
286 |
# if we are to align the descriptions. |
|
287 |
if [[ $maxdesclength -gt 8 ]]; then |
|
288 |
# Add the proper number of spaces to align the descriptions |
|
289 |
for ((i = ${#comp} ; i < longest ; i++)); do |
|
290 |
comp+=" " |
|
291 |
done |
|
292 |
else |
|
293 |
# Don't pad the descriptions so we can fit more text after the completion |
|
294 |
maxdesclength=$(( COLUMNS - ${#comp} - 4 )) |
|
295 |
fi |
256
|
296 |
|
260
|
297 |
# If there is enough space for any description text, |
|
298 |
# truncate the descriptions that are too long for the shell width |
|
299 |
if [ $maxdesclength -gt 0 ]; then |
|
300 |
if [ ${#desc} -gt $maxdesclength ]; then |
|
301 |
desc=${desc:0:$(( maxdesclength - 1 ))} |
|
302 |
desc+="…" |
|
303 |
fi |
|
304 |
comp+=" ($desc)" |
256
|
305 |
fi |
260
|
306 |
COMPREPLY[ci]=$comp |
|
307 |
__%[1]s_debug "Final comp: $comp" |
256
|
308 |
fi |
260
|
309 |
done |
256
|
310 |
} |
|
311 |
|
|
312 |
__start_%[1]s() |
|
313 |
{ |
|
314 |
local cur prev words cword split |
|
315 |
|
|
316 |
COMPREPLY=() |
|
317 |
|
|
318 |
# Call _init_completion from the bash-completion package |
|
319 |
# to prepare the arguments properly |
|
320 |
if declare -F _init_completion >/dev/null 2>&1; then |
|
321 |
_init_completion -n "=:" || return |
|
322 |
else |
|
323 |
__%[1]s_init_completion -n "=:" || return |
|
324 |
fi |
|
325 |
|
|
326 |
__%[1]s_debug |
|
327 |
__%[1]s_debug "========= starting completion logic ==========" |
|
328 |
__%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" |
|
329 |
|
|
330 |
# The user could have moved the cursor backwards on the command-line. |
|
331 |
# We need to trigger completion from the $cword location, so we need |
|
332 |
# to truncate the command-line ($words) up to the $cword location. |
|
333 |
words=("${words[@]:0:$cword+1}") |
|
334 |
__%[1]s_debug "Truncated words[*]: ${words[*]}," |
|
335 |
|
|
336 |
local out directive |
|
337 |
__%[1]s_get_completion_results |
|
338 |
__%[1]s_process_completion_results |
|
339 |
} |
|
340 |
|
|
341 |
if [[ $(type -t compopt) = "builtin" ]]; then |
|
342 |
complete -o default -F __start_%[1]s %[1]s |
|
343 |
else |
|
344 |
complete -o default -o nospace -F __start_%[1]s %[1]s |
|
345 |
fi |
|
346 |
|
|
347 |
# ex: ts=4 sw=4 et filetype=sh |
|
348 |
`, name, compCmd, |
|
349 |
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
260
|
350 |
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, |
|
351 |
activeHelpMarker)) |
256
|
352 |
} |
|
353 |
|
|
354 |
// GenBashCompletionFileV2 generates Bash completion version 2. |
|
355 |
func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error { |
|
356 |
outFile, err := os.Create(filename) |
|
357 |
if err != nil { |
|
358 |
return err |
|
359 |
} |
|
360 |
defer outFile.Close() |
|
361 |
|
|
362 |
return c.GenBashCompletionV2(outFile, includeDesc) |
|
363 |
} |
|
364 |
|
|
365 |
// GenBashCompletionV2 generates Bash completion file version 2 |
|
366 |
// and writes it to the passed writer. |
|
367 |
func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error { |
|
368 |
return c.genBashCompletion(w, includeDesc) |
|
369 |
} |