github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/cmd/completion/bash_completion.go (about)

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