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