bash_completionsV2.go (13493B)
1 // Copyright 2013-2023 The Cobra Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 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}" 95 __%[1]s_debug "The completions are: ${out}" 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 local shellCompDirectiveKeepOrder=%[8]d 105 106 if (((directive & shellCompDirectiveError) != 0)); then 107 # Error code. No completion. 108 __%[1]s_debug "Received error from custom completion go code" 109 return 110 else 111 if (((directive & shellCompDirectiveNoSpace) != 0)); then 112 if [[ $(type -t compopt) == builtin ]]; then 113 __%[1]s_debug "Activating no space" 114 compopt -o nospace 115 else 116 __%[1]s_debug "No space directive not supported in this version of bash" 117 fi 118 fi 119 if (((directive & shellCompDirectiveKeepOrder) != 0)); then 120 if [[ $(type -t compopt) == builtin ]]; then 121 # no sort isn't supported for bash less than < 4.4 122 if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then 123 __%[1]s_debug "No sort directive not supported in this version of bash" 124 else 125 __%[1]s_debug "Activating keep order" 126 compopt -o nosort 127 fi 128 else 129 __%[1]s_debug "No sort directive not supported in this version of bash" 130 fi 131 fi 132 if (((directive & shellCompDirectiveNoFileComp) != 0)); then 133 if [[ $(type -t compopt) == builtin ]]; then 134 __%[1]s_debug "Activating no file completion" 135 compopt +o default 136 else 137 __%[1]s_debug "No file completion directive not supported in this version of bash" 138 fi 139 fi 140 fi 141 142 # Separate activeHelp from normal completions 143 local completions=() 144 local activeHelp=() 145 __%[1]s_extract_activeHelp 146 147 if (((directive & shellCompDirectiveFilterFileExt) != 0)); then 148 # File extension filtering 149 local fullFilter filter filteringCmd 150 151 # Do not use quotes around the $completions variable or else newline 152 # characters will be kept. 153 for filter in ${completions[*]}; do 154 fullFilter+="$filter|" 155 done 156 157 filteringCmd="_filedir $fullFilter" 158 __%[1]s_debug "File filtering command: $filteringCmd" 159 $filteringCmd 160 elif (((directive & shellCompDirectiveFilterDirs) != 0)); then 161 # File completion for directories only 162 163 local subdir 164 subdir=${completions[0]} 165 if [[ -n $subdir ]]; then 166 __%[1]s_debug "Listing directories in $subdir" 167 pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return 168 else 169 __%[1]s_debug "Listing directories in ." 170 _filedir -d 171 fi 172 else 173 __%[1]s_handle_completion_types 174 fi 175 176 __%[1]s_handle_special_char "$cur" : 177 __%[1]s_handle_special_char "$cur" = 178 179 # Print the activeHelp statements before we finish 180 if ((${#activeHelp[*]} != 0)); then 181 printf "\n"; 182 printf "%%s\n" "${activeHelp[@]}" 183 printf "\n" 184 185 # The prompt format is only available from bash 4.4. 186 # We test if it is available before using it. 187 if (x=${PS1@P}) 2> /dev/null; then 188 printf "%%s" "${PS1@P}${COMP_LINE[@]}" 189 else 190 # Can't print the prompt. Just print the 191 # text the user had typed, it is workable enough. 192 printf "%%s" "${COMP_LINE[@]}" 193 fi 194 fi 195 } 196 197 # Separate activeHelp lines from real completions. 198 # Fills the $activeHelp and $completions arrays. 199 __%[1]s_extract_activeHelp() { 200 local activeHelpMarker="%[9]s" 201 local endIndex=${#activeHelpMarker} 202 203 while IFS='' read -r comp; do 204 if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then 205 comp=${comp:endIndex} 206 __%[1]s_debug "ActiveHelp found: $comp" 207 if [[ -n $comp ]]; then 208 activeHelp+=("$comp") 209 fi 210 else 211 # Not an activeHelp line but a normal completion 212 completions+=("$comp") 213 fi 214 done <<<"${out}" 215 } 216 217 __%[1]s_handle_completion_types() { 218 __%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE" 219 220 case $COMP_TYPE in 221 37|42) 222 # Type: menu-complete/menu-complete-backward and insert-completions 223 # If the user requested inserting one completion at a time, or all 224 # completions at once on the command-line we must remove the descriptions. 225 # https://github.com/spf13/cobra/issues/1508 226 local tab=$'\t' comp 227 while IFS='' read -r comp; do 228 [[ -z $comp ]] && continue 229 # Strip any description 230 comp=${comp%%%%$tab*} 231 # Only consider the completions that match 232 if [[ $comp == "$cur"* ]]; then 233 COMPREPLY+=("$comp") 234 fi 235 done < <(printf "%%s\n" "${completions[@]}") 236 ;; 237 238 *) 239 # Type: complete (normal completion) 240 __%[1]s_handle_standard_completion_case 241 ;; 242 esac 243 } 244 245 __%[1]s_handle_standard_completion_case() { 246 local tab=$'\t' comp 247 248 # Short circuit to optimize if we don't have descriptions 249 if [[ "${completions[*]}" != *$tab* ]]; then 250 IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") 251 return 0 252 fi 253 254 local longest=0 255 local compline 256 # Look for the longest completion so that we can format things nicely 257 while IFS='' read -r compline; do 258 [[ -z $compline ]] && continue 259 # Strip any description before checking the length 260 comp=${compline%%%%$tab*} 261 # Only consider the completions that match 262 [[ $comp == "$cur"* ]] || continue 263 COMPREPLY+=("$compline") 264 if ((${#comp}>longest)); then 265 longest=${#comp} 266 fi 267 done < <(printf "%%s\n" "${completions[@]}") 268 269 # If there is a single completion left, remove the description text 270 if ((${#COMPREPLY[*]} == 1)); then 271 __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" 272 comp="${COMPREPLY[0]%%%%$tab*}" 273 __%[1]s_debug "Removed description from single completion, which is now: ${comp}" 274 COMPREPLY[0]=$comp 275 else # Format the descriptions 276 __%[1]s_format_comp_descriptions $longest 277 fi 278 } 279 280 __%[1]s_handle_special_char() 281 { 282 local comp="$1" 283 local char=$2 284 if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then 285 local word=${comp%%"${comp##*${char}}"} 286 local idx=${#COMPREPLY[*]} 287 while ((--idx >= 0)); do 288 COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} 289 done 290 fi 291 } 292 293 __%[1]s_format_comp_descriptions() 294 { 295 local tab=$'\t' 296 local comp desc maxdesclength 297 local longest=$1 298 299 local i ci 300 for ci in ${!COMPREPLY[*]}; do 301 comp=${COMPREPLY[ci]} 302 # Properly format the description string which follows a tab character if there is one 303 if [[ "$comp" == *$tab* ]]; then 304 __%[1]s_debug "Original comp: $comp" 305 desc=${comp#*$tab} 306 comp=${comp%%%%$tab*} 307 308 # $COLUMNS stores the current shell width. 309 # Remove an extra 4 because we add 2 spaces and 2 parentheses. 310 maxdesclength=$(( COLUMNS - longest - 4 )) 311 312 # Make sure we can fit a description of at least 8 characters 313 # if we are to align the descriptions. 314 if ((maxdesclength > 8)); then 315 # Add the proper number of spaces to align the descriptions 316 for ((i = ${#comp} ; i < longest ; i++)); do 317 comp+=" " 318 done 319 else 320 # Don't pad the descriptions so we can fit more text after the completion 321 maxdesclength=$(( COLUMNS - ${#comp} - 4 )) 322 fi 323 324 # If there is enough space for any description text, 325 # truncate the descriptions that are too long for the shell width 326 if ((maxdesclength > 0)); then 327 if ((${#desc} > maxdesclength)); then 328 desc=${desc:0:$(( maxdesclength - 1 ))} 329 desc+="…" 330 fi 331 comp+=" ($desc)" 332 fi 333 COMPREPLY[ci]=$comp 334 __%[1]s_debug "Final comp: $comp" 335 fi 336 done 337 } 338 339 __start_%[1]s() 340 { 341 local cur prev words cword split 342 343 COMPREPLY=() 344 345 # Call _init_completion from the bash-completion package 346 # to prepare the arguments properly 347 if declare -F _init_completion >/dev/null 2>&1; then 348 _init_completion -n =: || return 349 else 350 __%[1]s_init_completion -n =: || return 351 fi 352 353 __%[1]s_debug 354 __%[1]s_debug "========= starting completion logic ==========" 355 __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" 356 357 # The user could have moved the cursor backwards on the command-line. 358 # We need to trigger completion from the $cword location, so we need 359 # to truncate the command-line ($words) up to the $cword location. 360 words=("${words[@]:0:$cword+1}") 361 __%[1]s_debug "Truncated words[*]: ${words[*]}," 362 363 local out directive 364 __%[1]s_get_completion_results 365 __%[1]s_process_completion_results 366 } 367 368 if [[ $(type -t compopt) = "builtin" ]]; then 369 complete -o default -F __start_%[1]s %[1]s 370 else 371 complete -o default -o nospace -F __start_%[1]s %[1]s 372 fi 373 374 # ex: ts=4 sw=4 et filetype=sh 375 `, name, compCmd, 376 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 377 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, 378 activeHelpMarker)) 379 } 380 381 // GenBashCompletionFileV2 generates Bash completion version 2. 382 func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error { 383 outFile, err := os.Create(filename) 384 if err != nil { 385 return err 386 } 387 defer outFile.Close() 388 389 return c.GenBashCompletionV2(outFile, includeDesc) 390 } 391 392 // GenBashCompletionV2 generates Bash completion file version 2 393 // and writes it to the passed writer. 394 func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error { 395 return c.genBashCompletion(w, includeDesc) 396 }