|
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}" |
|
81 __%[1]s_debug "The completions are: ${out[*]}" |
|
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 |
|
114 if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then |
|
115 # File extension filtering |
|
116 local fullFilter filter filteringCmd |
|
117 |
|
118 # Do not use quotes around the $out variable or else newline |
|
119 # characters will be kept. |
|
120 for filter in ${out[*]}; do |
|
121 fullFilter+="$filter|" |
|
122 done |
|
123 |
|
124 filteringCmd="_filedir $fullFilter" |
|
125 __%[1]s_debug "File filtering command: $filteringCmd" |
|
126 $filteringCmd |
|
127 elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then |
|
128 # File completion for directories only |
|
129 |
|
130 # Use printf to strip any trailing newline |
|
131 local subdir |
|
132 subdir=$(printf "%%s" "${out[0]}") |
|
133 if [ -n "$subdir" ]; then |
|
134 __%[1]s_debug "Listing directories in $subdir" |
|
135 pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return |
|
136 else |
|
137 __%[1]s_debug "Listing directories in ." |
|
138 _filedir -d |
|
139 fi |
|
140 else |
|
141 __%[1]s_handle_standard_completion_case |
|
142 fi |
|
143 |
|
144 __%[1]s_handle_special_char "$cur" : |
|
145 __%[1]s_handle_special_char "$cur" = |
|
146 } |
|
147 |
|
148 __%[1]s_handle_standard_completion_case() { |
|
149 local tab comp |
|
150 tab=$(printf '\t') |
|
151 |
|
152 local longest=0 |
|
153 # Look for the longest completion so that we can format things nicely |
|
154 while IFS='' read -r comp; do |
|
155 # Strip any description before checking the length |
|
156 comp=${comp%%%%$tab*} |
|
157 # Only consider the completions that match |
|
158 comp=$(compgen -W "$comp" -- "$cur") |
|
159 if ((${#comp}>longest)); then |
|
160 longest=${#comp} |
|
161 fi |
|
162 done < <(printf "%%s\n" "${out[@]}") |
|
163 |
|
164 local completions=() |
|
165 while IFS='' read -r comp; do |
|
166 if [ -z "$comp" ]; then |
|
167 continue |
|
168 fi |
|
169 |
|
170 __%[1]s_debug "Original comp: $comp" |
|
171 comp="$(__%[1]s_format_comp_descriptions "$comp" "$longest")" |
|
172 __%[1]s_debug "Final comp: $comp" |
|
173 completions+=("$comp") |
|
174 done < <(printf "%%s\n" "${out[@]}") |
|
175 |
|
176 while IFS='' read -r comp; do |
|
177 COMPREPLY+=("$comp") |
|
178 done < <(compgen -W "${completions[*]}" -- "$cur") |
|
179 |
|
180 # If there is a single completion left, remove the description text |
|
181 if [ ${#COMPREPLY[*]} -eq 1 ]; then |
|
182 __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" |
|
183 comp="${COMPREPLY[0]%%%% *}" |
|
184 __%[1]s_debug "Removed description from single completion, which is now: ${comp}" |
|
185 COMPREPLY=() |
|
186 COMPREPLY+=("$comp") |
|
187 fi |
|
188 } |
|
189 |
|
190 __%[1]s_handle_special_char() |
|
191 { |
|
192 local comp="$1" |
|
193 local char=$2 |
|
194 if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then |
|
195 local word=${comp%%"${comp##*${char}}"} |
|
196 local idx=${#COMPREPLY[*]} |
|
197 while [[ $((--idx)) -ge 0 ]]; do |
|
198 COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} |
|
199 done |
|
200 fi |
|
201 } |
|
202 |
|
203 __%[1]s_format_comp_descriptions() |
|
204 { |
|
205 local tab |
|
206 tab=$(printf '\t') |
|
207 local comp="$1" |
|
208 local longest=$2 |
|
209 |
|
210 # Properly format the description string which follows a tab character if there is one |
|
211 if [[ "$comp" == *$tab* ]]; then |
|
212 desc=${comp#*$tab} |
|
213 comp=${comp%%%%$tab*} |
|
214 |
|
215 # $COLUMNS stores the current shell width. |
|
216 # Remove an extra 4 because we add 2 spaces and 2 parentheses. |
|
217 maxdesclength=$(( COLUMNS - longest - 4 )) |
|
218 |
|
219 # Make sure we can fit a description of at least 8 characters |
|
220 # if we are to align the descriptions. |
|
221 if [[ $maxdesclength -gt 8 ]]; then |
|
222 # Add the proper number of spaces to align the descriptions |
|
223 for ((i = ${#comp} ; i < longest ; i++)); do |
|
224 comp+=" " |
|
225 done |
|
226 else |
|
227 # Don't pad the descriptions so we can fit more text after the completion |
|
228 maxdesclength=$(( COLUMNS - ${#comp} - 4 )) |
|
229 fi |
|
230 |
|
231 # If there is enough space for any description text, |
|
232 # truncate the descriptions that are too long for the shell width |
|
233 if [ $maxdesclength -gt 0 ]; then |
|
234 if [ ${#desc} -gt $maxdesclength ]; then |
|
235 desc=${desc:0:$(( maxdesclength - 1 ))} |
|
236 desc+="…" |
|
237 fi |
|
238 comp+=" ($desc)" |
|
239 fi |
|
240 fi |
|
241 |
|
242 # Must use printf to escape all special characters |
|
243 printf "%%q" "${comp}" |
|
244 } |
|
245 |
|
246 __start_%[1]s() |
|
247 { |
|
248 local cur prev words cword split |
|
249 |
|
250 COMPREPLY=() |
|
251 |
|
252 # Call _init_completion from the bash-completion package |
|
253 # to prepare the arguments properly |
|
254 if declare -F _init_completion >/dev/null 2>&1; then |
|
255 _init_completion -n "=:" || return |
|
256 else |
|
257 __%[1]s_init_completion -n "=:" || return |
|
258 fi |
|
259 |
|
260 __%[1]s_debug |
|
261 __%[1]s_debug "========= starting completion logic ==========" |
|
262 __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" |
|
263 |
|
264 # The user could have moved the cursor backwards on the command-line. |
|
265 # We need to trigger completion from the $cword location, so we need |
|
266 # to truncate the command-line ($words) up to the $cword location. |
|
267 words=("${words[@]:0:$cword+1}") |
|
268 __%[1]s_debug "Truncated words[*]: ${words[*]}," |
|
269 |
|
270 local out directive |
|
271 __%[1]s_get_completion_results |
|
272 __%[1]s_process_completion_results |
|
273 } |
|
274 |
|
275 if [[ $(type -t compopt) = "builtin" ]]; then |
|
276 complete -o default -F __start_%[1]s %[1]s |
|
277 else |
|
278 complete -o default -o nospace -F __start_%[1]s %[1]s |
|
279 fi |
|
280 |
|
281 # ex: ts=4 sw=4 et filetype=sh |
|
282 `, name, compCmd, |
|
283 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
|
284 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) |
|
285 } |
|
286 |
|
287 // GenBashCompletionFileV2 generates Bash completion version 2. |
|
288 func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error { |
|
289 outFile, err := os.Create(filename) |
|
290 if err != nil { |
|
291 return err |
|
292 } |
|
293 defer outFile.Close() |
|
294 |
|
295 return c.GenBashCompletionV2(outFile, includeDesc) |
|
296 } |
|
297 |
|
298 // GenBashCompletionV2 generates Bash completion file version 2 |
|
299 // and writes it to the passed writer. |
|
300 func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error { |
|
301 return c.genBashCompletion(w, includeDesc) |
|
302 } |