git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cobra/bash_completions.go (about) 1 // Copyright 2013-2022 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 "sort" 23 "strings" 24 25 "github.com/spf13/pflag" 26 ) 27 28 // Annotations for Bash completion. 29 const ( 30 BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions" 31 BashCompCustom = "cobra_annotation_bash_completion_custom" 32 BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" 33 BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" 34 ) 35 36 func writePreamble(buf io.StringWriter, name string) { 37 WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name)) 38 WriteStringAndCheck(buf, fmt.Sprintf(` 39 __%[1]s_debug() 40 { 41 if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then 42 echo "$*" >> "${BASH_COMP_DEBUG_FILE}" 43 fi 44 } 45 46 # Homebrew on Macs have version 1.3 of bash-completion which doesn't include 47 # _init_completion. This is a very 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 __%[1]s_index_of_word() 55 { 56 local w word=$1 57 shift 58 index=0 59 for w in "$@"; do 60 [[ $w = "$word" ]] && return 61 index=$((index+1)) 62 done 63 index=-1 64 } 65 66 __%[1]s_contains_word() 67 { 68 local w word=$1; shift 69 for w in "$@"; do 70 [[ $w = "$word" ]] && return 71 done 72 return 1 73 } 74 75 __%[1]s_handle_go_custom_completion() 76 { 77 __%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" 78 79 local shellCompDirectiveError=%[3]d 80 local shellCompDirectiveNoSpace=%[4]d 81 local shellCompDirectiveNoFileComp=%[5]d 82 local shellCompDirectiveFilterFileExt=%[6]d 83 local shellCompDirectiveFilterDirs=%[7]d 84 85 local out requestComp lastParam lastChar comp directive args 86 87 # Prepare the command to request completions for the program. 88 # Calling ${words[0]} instead of directly %[1]s allows to handle aliases 89 args=("${words[@]:1}") 90 # Disable ActiveHelp which is not supported for bash completion v1 91 requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}" 92 93 lastParam=${words[$((${#words[@]}-1))]} 94 lastChar=${lastParam:$((${#lastParam}-1)):1} 95 __%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" 96 97 if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then 98 # If the last parameter is complete (there is a space following it) 99 # We add an extra empty parameter so we can indicate this to the go method. 100 __%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter" 101 requestComp="${requestComp} \"\"" 102 fi 103 104 __%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}" 105 # Use eval to handle any environment variables and such 106 out=$(eval "${requestComp}" 2>/dev/null) 107 108 # Extract the directive integer at the very end of the output following a colon (:) 109 directive=${out##*:} 110 # Remove the directive 111 out=${out%%:*} 112 if [ "${directive}" = "${out}" ]; then 113 # There is not directive specified 114 directive=0 115 fi 116 __%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" 117 __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out}" 118 119 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 120 # Error code. No completion. 121 __%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code" 122 return 123 else 124 if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 125 if [[ $(type -t compopt) = "builtin" ]]; then 126 __%[1]s_debug "${FUNCNAME[0]}: activating no space" 127 compopt -o nospace 128 fi 129 fi 130 if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 131 if [[ $(type -t compopt) = "builtin" ]]; then 132 __%[1]s_debug "${FUNCNAME[0]}: activating no file completion" 133 compopt +o default 134 fi 135 fi 136 fi 137 138 if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 139 # File extension filtering 140 local fullFilter filter filteringCmd 141 # Do not use quotes around the $out variable or else newline 142 # characters will be kept. 143 for filter in ${out}; do 144 fullFilter+="$filter|" 145 done 146 147 filteringCmd="_filedir $fullFilter" 148 __%[1]s_debug "File filtering command: $filteringCmd" 149 $filteringCmd 150 elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 151 # File completion for directories only 152 local subdir 153 # Use printf to strip any trailing newline 154 subdir=$(printf "%%s" "${out}") 155 if [ -n "$subdir" ]; then 156 __%[1]s_debug "Listing directories in $subdir" 157 __%[1]s_handle_subdirs_in_dir_flag "$subdir" 158 else 159 __%[1]s_debug "Listing directories in ." 160 _filedir -d 161 fi 162 else 163 while IFS='' read -r comp; do 164 COMPREPLY+=("$comp") 165 done < <(compgen -W "${out}" -- "$cur") 166 fi 167 } 168 169 __%[1]s_handle_reply() 170 { 171 __%[1]s_debug "${FUNCNAME[0]}" 172 local comp 173 case $cur in 174 -*) 175 if [[ $(type -t compopt) = "builtin" ]]; then 176 compopt -o nospace 177 fi 178 local allflags 179 if [ ${#must_have_one_flag[@]} -ne 0 ]; then 180 allflags=("${must_have_one_flag[@]}") 181 else 182 allflags=("${flags[*]} ${two_word_flags[*]}") 183 fi 184 while IFS='' read -r comp; do 185 COMPREPLY+=("$comp") 186 done < <(compgen -W "${allflags[*]}" -- "$cur") 187 if [[ $(type -t compopt) = "builtin" ]]; then 188 [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace 189 fi 190 191 # complete after --flag=abc 192 if [[ $cur == *=* ]]; then 193 if [[ $(type -t compopt) = "builtin" ]]; then 194 compopt +o nospace 195 fi 196 197 local index flag 198 flag="${cur%%=*}" 199 __%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}" 200 COMPREPLY=() 201 if [[ ${index} -ge 0 ]]; then 202 PREFIX="" 203 cur="${cur#*=}" 204 ${flags_completion[${index}]} 205 if [ -n "${ZSH_VERSION:-}" ]; then 206 # zsh completion needs --flag= prefix 207 eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" 208 fi 209 fi 210 fi 211 212 if [[ -z "${flag_parsing_disabled}" ]]; then 213 # If flag parsing is enabled, we have completed the flags and can return. 214 # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough 215 # to possibly call handle_go_custom_completion. 216 return 0; 217 fi 218 ;; 219 esac 220 221 # check if we are handling a flag with special work handling 222 local index 223 __%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}" 224 if [[ ${index} -ge 0 ]]; then 225 ${flags_completion[${index}]} 226 return 227 fi 228 229 # we are parsing a flag and don't have a special handler, no completion 230 if [[ ${cur} != "${words[cword]}" ]]; then 231 return 232 fi 233 234 local completions 235 completions=("${commands[@]}") 236 if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then 237 completions+=("${must_have_one_noun[@]}") 238 elif [[ -n "${has_completion_function}" ]]; then 239 # if a go completion function is provided, defer to that function 240 __%[1]s_handle_go_custom_completion 241 fi 242 if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then 243 completions+=("${must_have_one_flag[@]}") 244 fi 245 while IFS='' read -r comp; do 246 COMPREPLY+=("$comp") 247 done < <(compgen -W "${completions[*]}" -- "$cur") 248 249 if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then 250 while IFS='' read -r comp; do 251 COMPREPLY+=("$comp") 252 done < <(compgen -W "${noun_aliases[*]}" -- "$cur") 253 fi 254 255 if [[ ${#COMPREPLY[@]} -eq 0 ]]; then 256 if declare -F __%[1]s_custom_func >/dev/null; then 257 # try command name qualified custom func 258 __%[1]s_custom_func 259 else 260 # otherwise fall back to unqualified for compatibility 261 declare -F __custom_func >/dev/null && __custom_func 262 fi 263 fi 264 265 # available in bash-completion >= 2, not always present on macOS 266 if declare -F __ltrim_colon_completions >/dev/null; then 267 __ltrim_colon_completions "$cur" 268 fi 269 270 # If there is only 1 completion and it is a flag with an = it will be completed 271 # but we don't want a space after the = 272 if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then 273 compopt -o nospace 274 fi 275 } 276 277 # The arguments should be in the form "ext1|ext2|extn" 278 __%[1]s_handle_filename_extension_flag() 279 { 280 local ext="$1" 281 _filedir "@(${ext})" 282 } 283 284 __%[1]s_handle_subdirs_in_dir_flag() 285 { 286 local dir="$1" 287 pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return 288 } 289 290 __%[1]s_handle_flag() 291 { 292 __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" 293 294 # if a command required a flag, and we found it, unset must_have_one_flag() 295 local flagname=${words[c]} 296 local flagvalue="" 297 # if the word contained an = 298 if [[ ${words[c]} == *"="* ]]; then 299 flagvalue=${flagname#*=} # take in as flagvalue after the = 300 flagname=${flagname%%=*} # strip everything after the = 301 flagname="${flagname}=" # but put the = back 302 fi 303 __%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}" 304 if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then 305 must_have_one_flag=() 306 fi 307 308 # if you set a flag which only applies to this command, don't show subcommands 309 if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then 310 commands=() 311 fi 312 313 # keep flag value with flagname as flaghash 314 # flaghash variable is an associative array which is only supported in bash > 3. 315 if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then 316 if [ -n "${flagvalue}" ] ; then 317 flaghash[${flagname}]=${flagvalue} 318 elif [ -n "${words[ $((c+1)) ]}" ] ; then 319 flaghash[${flagname}]=${words[ $((c+1)) ]} 320 else 321 flaghash[${flagname}]="true" # pad "true" for bool flag 322 fi 323 fi 324 325 # skip the argument to a two word flag 326 if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then 327 __%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" 328 c=$((c+1)) 329 # if we are looking for a flags value, don't show commands 330 if [[ $c -eq $cword ]]; then 331 commands=() 332 fi 333 fi 334 335 c=$((c+1)) 336 337 } 338 339 __%[1]s_handle_noun() 340 { 341 __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" 342 343 if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then 344 must_have_one_noun=() 345 elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then 346 must_have_one_noun=() 347 fi 348 349 nouns+=("${words[c]}") 350 c=$((c+1)) 351 } 352 353 __%[1]s_handle_command() 354 { 355 __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" 356 357 local next_command 358 if [[ -n ${last_command} ]]; then 359 next_command="_${last_command}_${words[c]//:/__}" 360 else 361 if [[ $c -eq 0 ]]; then 362 next_command="_%[1]s_root_command" 363 else 364 next_command="_${words[c]//:/__}" 365 fi 366 fi 367 c=$((c+1)) 368 __%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}" 369 declare -F "$next_command" >/dev/null && $next_command 370 } 371 372 __%[1]s_handle_word() 373 { 374 if [[ $c -ge $cword ]]; then 375 __%[1]s_handle_reply 376 return 377 fi 378 __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" 379 if [[ "${words[c]}" == -* ]]; then 380 __%[1]s_handle_flag 381 elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then 382 __%[1]s_handle_command 383 elif [[ $c -eq 0 ]]; then 384 __%[1]s_handle_command 385 elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then 386 # aliashash variable is an associative array which is only supported in bash > 3. 387 if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then 388 words[c]=${aliashash[${words[c]}]} 389 __%[1]s_handle_command 390 else 391 __%[1]s_handle_noun 392 fi 393 else 394 __%[1]s_handle_noun 395 fi 396 __%[1]s_handle_word 397 } 398 399 `, name, ShellCompNoDescRequestCmd, 400 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 401 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name))) 402 } 403 404 func writePostscript(buf io.StringWriter, name string) { 405 name = strings.ReplaceAll(name, ":", "__") 406 WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name)) 407 WriteStringAndCheck(buf, fmt.Sprintf(`{ 408 local cur prev words cword split 409 declare -A flaghash 2>/dev/null || : 410 declare -A aliashash 2>/dev/null || : 411 if declare -F _init_completion >/dev/null 2>&1; then 412 _init_completion -s || return 413 else 414 __%[1]s_init_completion -n "=" || return 415 fi 416 417 local c=0 418 local flag_parsing_disabled= 419 local flags=() 420 local two_word_flags=() 421 local local_nonpersistent_flags=() 422 local flags_with_completion=() 423 local flags_completion=() 424 local commands=("%[1]s") 425 local command_aliases=() 426 local must_have_one_flag=() 427 local must_have_one_noun=() 428 local has_completion_function="" 429 local last_command="" 430 local nouns=() 431 local noun_aliases=() 432 433 __%[1]s_handle_word 434 } 435 436 `, name)) 437 WriteStringAndCheck(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then 438 complete -o default -F __start_%s %s 439 else 440 complete -o default -o nospace -F __start_%s %s 441 fi 442 443 `, name, name, name, name)) 444 WriteStringAndCheck(buf, "# ex: ts=4 sw=4 et filetype=sh\n") 445 } 446 447 func writeCommands(buf io.StringWriter, cmd *Command) { 448 WriteStringAndCheck(buf, " commands=()\n") 449 for _, c := range cmd.Commands() { 450 if !c.IsAvailableCommand() && c != cmd.helpCommand { 451 continue 452 } 453 WriteStringAndCheck(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name())) 454 writeCmdAliases(buf, c) 455 } 456 WriteStringAndCheck(buf, "\n") 457 } 458 459 func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *Command) { 460 for key, value := range annotations { 461 switch key { 462 case BashCompFilenameExt: 463 WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) 464 465 var ext string 466 if len(value) > 0 { 467 ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Root().Name()) + strings.Join(value, "|") 468 } else { 469 ext = "_filedir" 470 } 471 WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) 472 case BashCompCustom: 473 WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) 474 475 if len(value) > 0 { 476 handlers := strings.Join(value, "; ") 477 WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers)) 478 } else { 479 WriteStringAndCheck(buf, " flags_completion+=(:)\n") 480 } 481 case BashCompSubdirsInDir: 482 WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) 483 484 var ext string 485 if len(value) == 1 { 486 ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Root().Name()) + value[0] 487 } else { 488 ext = "_filedir -d" 489 } 490 WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) 491 } 492 } 493 } 494 495 const cbn = "\")\n" 496 497 func writeShortFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) { 498 name := flag.Shorthand 499 format := " " 500 if len(flag.NoOptDefVal) == 0 { 501 format += "two_word_" 502 } 503 format += "flags+=(\"-%s" + cbn 504 WriteStringAndCheck(buf, fmt.Sprintf(format, name)) 505 writeFlagHandler(buf, "-"+name, flag.Annotations, cmd) 506 } 507 508 func writeFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) { 509 name := flag.Name 510 format := " flags+=(\"--%s" 511 if len(flag.NoOptDefVal) == 0 { 512 format += "=" 513 } 514 format += cbn 515 WriteStringAndCheck(buf, fmt.Sprintf(format, name)) 516 if len(flag.NoOptDefVal) == 0 { 517 format = " two_word_flags+=(\"--%s" + cbn 518 WriteStringAndCheck(buf, fmt.Sprintf(format, name)) 519 } 520 writeFlagHandler(buf, "--"+name, flag.Annotations, cmd) 521 } 522 523 func writeLocalNonPersistentFlag(buf io.StringWriter, flag *pflag.Flag) { 524 name := flag.Name 525 format := " local_nonpersistent_flags+=(\"--%[1]s" + cbn 526 if len(flag.NoOptDefVal) == 0 { 527 format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn 528 } 529 WriteStringAndCheck(buf, fmt.Sprintf(format, name)) 530 if len(flag.Shorthand) > 0 { 531 WriteStringAndCheck(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%s\")\n", flag.Shorthand)) 532 } 533 } 534 535 // Setup annotations for go completions for registered flags 536 func prepareCustomAnnotationsForFlags(cmd *Command) { 537 flagCompletionMutex.RLock() 538 defer flagCompletionMutex.RUnlock() 539 for flag := range flagCompletionFunctions { 540 // Make sure the completion script calls the __*_go_custom_completion function for 541 // every registered flag. We need to do this here (and not when the flag was registered 542 // for completion) so that we can know the root command name for the prefix 543 // of __<prefix>_go_custom_completion 544 if flag.Annotations == nil { 545 flag.Annotations = map[string][]string{} 546 } 547 flag.Annotations[BashCompCustom] = []string{fmt.Sprintf("__%[1]s_handle_go_custom_completion", cmd.Root().Name())} 548 } 549 } 550 551 func writeFlags(buf io.StringWriter, cmd *Command) { 552 prepareCustomAnnotationsForFlags(cmd) 553 WriteStringAndCheck(buf, ` flags=() 554 two_word_flags=() 555 local_nonpersistent_flags=() 556 flags_with_completion=() 557 flags_completion=() 558 559 `) 560 561 if cmd.DisableFlagParsing { 562 WriteStringAndCheck(buf, " flag_parsing_disabled=1\n") 563 } 564 565 localNonPersistentFlags := cmd.LocalNonPersistentFlags() 566 cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { 567 if nonCompletableFlag(flag) { 568 return 569 } 570 writeFlag(buf, flag, cmd) 571 if len(flag.Shorthand) > 0 { 572 writeShortFlag(buf, flag, cmd) 573 } 574 // localNonPersistentFlags are used to stop the completion of subcommands when one is set 575 // if TraverseChildren is true we should allow to complete subcommands 576 if localNonPersistentFlags.Lookup(flag.Name) != nil && !cmd.Root().TraverseChildren { 577 writeLocalNonPersistentFlag(buf, flag) 578 } 579 }) 580 cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { 581 if nonCompletableFlag(flag) { 582 return 583 } 584 writeFlag(buf, flag, cmd) 585 if len(flag.Shorthand) > 0 { 586 writeShortFlag(buf, flag, cmd) 587 } 588 }) 589 590 WriteStringAndCheck(buf, "\n") 591 } 592 593 func writeRequiredFlag(buf io.StringWriter, cmd *Command) { 594 WriteStringAndCheck(buf, " must_have_one_flag=()\n") 595 flags := cmd.NonInheritedFlags() 596 flags.VisitAll(func(flag *pflag.Flag) { 597 if nonCompletableFlag(flag) { 598 return 599 } 600 for key := range flag.Annotations { 601 switch key { 602 case BashCompOneRequiredFlag: 603 format := " must_have_one_flag+=(\"--%s" 604 if flag.Value.Type() != "bool" { 605 format += "=" 606 } 607 format += cbn 608 WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name)) 609 610 if len(flag.Shorthand) > 0 { 611 WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand)) 612 } 613 } 614 } 615 }) 616 } 617 618 func writeRequiredNouns(buf io.StringWriter, cmd *Command) { 619 WriteStringAndCheck(buf, " must_have_one_noun=()\n") 620 sort.Strings(cmd.ValidArgs) 621 for _, value := range cmd.ValidArgs { 622 // Remove any description that may be included following a tab character. 623 // Descriptions are not supported by bash completion. 624 value = strings.Split(value, "\t")[0] 625 WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_noun+=(%q)\n", value)) 626 } 627 if cmd.ValidArgsFunction != nil { 628 WriteStringAndCheck(buf, " has_completion_function=1\n") 629 } 630 } 631 632 func writeCmdAliases(buf io.StringWriter, cmd *Command) { 633 if len(cmd.Aliases) == 0 { 634 return 635 } 636 637 sort.Strings(cmd.Aliases) 638 639 WriteStringAndCheck(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n")) 640 for _, value := range cmd.Aliases { 641 WriteStringAndCheck(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value)) 642 WriteStringAndCheck(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name())) 643 } 644 WriteStringAndCheck(buf, ` fi`) 645 WriteStringAndCheck(buf, "\n") 646 } 647 func writeArgAliases(buf io.StringWriter, cmd *Command) { 648 WriteStringAndCheck(buf, " noun_aliases=()\n") 649 sort.Strings(cmd.ArgAliases) 650 for _, value := range cmd.ArgAliases { 651 WriteStringAndCheck(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value)) 652 } 653 } 654 655 func gen(buf io.StringWriter, cmd *Command) { 656 for _, c := range cmd.Commands() { 657 if !c.IsAvailableCommand() && c != cmd.helpCommand { 658 continue 659 } 660 gen(buf, c) 661 } 662 commandName := cmd.CommandPath() 663 commandName = strings.ReplaceAll(commandName, " ", "_") 664 commandName = strings.ReplaceAll(commandName, ":", "__") 665 666 if cmd.Root() == cmd { 667 WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName)) 668 } else { 669 WriteStringAndCheck(buf, fmt.Sprintf("_%s()\n{\n", commandName)) 670 } 671 672 WriteStringAndCheck(buf, fmt.Sprintf(" last_command=%q\n", commandName)) 673 WriteStringAndCheck(buf, "\n") 674 WriteStringAndCheck(buf, " command_aliases=()\n") 675 WriteStringAndCheck(buf, "\n") 676 677 writeCommands(buf, cmd) 678 writeFlags(buf, cmd) 679 writeRequiredFlag(buf, cmd) 680 writeRequiredNouns(buf, cmd) 681 writeArgAliases(buf, cmd) 682 WriteStringAndCheck(buf, "}\n\n") 683 } 684 685 // GenBashCompletion generates bash completion file and writes to the passed writer. 686 func (c *Command) GenBashCompletion(w io.Writer) error { 687 buf := new(bytes.Buffer) 688 writePreamble(buf, c.Name()) 689 if len(c.BashCompletionFunction) > 0 { 690 buf.WriteString(c.BashCompletionFunction + "\n") 691 } 692 gen(buf, c) 693 writePostscript(buf, c.Name()) 694 695 _, err := buf.WriteTo(w) 696 return err 697 } 698 699 func nonCompletableFlag(flag *pflag.Flag) bool { 700 return flag.Hidden || len(flag.Deprecated) > 0 701 } 702 703 // GenBashCompletionFile generates bash completion file. 704 func (c *Command) GenBashCompletionFile(filename string) error { 705 outFile, err := os.Create(filename) 706 if err != nil { 707 return err 708 } 709 defer outFile.Close() 710 711 return c.GenBashCompletion(outFile) 712 }