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  }