...

Source file src/github.com/spf13/cobra/zsh_completions.go

Documentation: github.com/spf13/cobra

     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  // GenZshCompletionFile generates zsh completion file including descriptions.
    25  func (c *Command) GenZshCompletionFile(filename string) error {
    26  	return c.genZshCompletionFile(filename, true)
    27  }
    28  
    29  // GenZshCompletion generates zsh completion file including descriptions
    30  // and writes it to the passed writer.
    31  func (c *Command) GenZshCompletion(w io.Writer) error {
    32  	return c.genZshCompletion(w, true)
    33  }
    34  
    35  // GenZshCompletionFileNoDesc generates zsh completion file without descriptions.
    36  func (c *Command) GenZshCompletionFileNoDesc(filename string) error {
    37  	return c.genZshCompletionFile(filename, false)
    38  }
    39  
    40  // GenZshCompletionNoDesc generates zsh completion file without descriptions
    41  // and writes it to the passed writer.
    42  func (c *Command) GenZshCompletionNoDesc(w io.Writer) error {
    43  	return c.genZshCompletion(w, false)
    44  }
    45  
    46  // MarkZshCompPositionalArgumentFile only worked for zsh and its behavior was
    47  // not consistent with Bash completion. It has therefore been disabled.
    48  // Instead, when no other completion is specified, file completion is done by
    49  // default for every argument. One can disable file completion on a per-argument
    50  // basis by using ValidArgsFunction and ShellCompDirectiveNoFileComp.
    51  // To achieve file extension filtering, one can use ValidArgsFunction and
    52  // ShellCompDirectiveFilterFileExt.
    53  //
    54  // Deprecated
    55  func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error {
    56  	return nil
    57  }
    58  
    59  // MarkZshCompPositionalArgumentWords only worked for zsh. It has therefore
    60  // been disabled.
    61  // To achieve the same behavior across all shells, one can use
    62  // ValidArgs (for the first argument only) or ValidArgsFunction for
    63  // any argument (can include the first one also).
    64  //
    65  // Deprecated
    66  func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error {
    67  	return nil
    68  }
    69  
    70  func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error {
    71  	outFile, err := os.Create(filename)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	defer outFile.Close()
    76  
    77  	return c.genZshCompletion(outFile, includeDesc)
    78  }
    79  
    80  func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error {
    81  	buf := new(bytes.Buffer)
    82  	genZshComp(buf, c.Name(), includeDesc)
    83  	_, err := buf.WriteTo(w)
    84  	return err
    85  }
    86  
    87  func genZshComp(buf io.StringWriter, name string, includeDesc bool) {
    88  	compCmd := ShellCompRequestCmd
    89  	if !includeDesc {
    90  		compCmd = ShellCompNoDescRequestCmd
    91  	}
    92  	WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[1]s
    93  compdef _%[1]s %[1]s
    94  
    95  # zsh completion for %-36[1]s -*- shell-script -*-
    96  
    97  __%[1]s_debug()
    98  {
    99      local file="$BASH_COMP_DEBUG_FILE"
   100      if [[ -n ${file} ]]; then
   101          echo "$*" >> "${file}"
   102      fi
   103  }
   104  
   105  _%[1]s()
   106  {
   107      local shellCompDirectiveError=%[3]d
   108      local shellCompDirectiveNoSpace=%[4]d
   109      local shellCompDirectiveNoFileComp=%[5]d
   110      local shellCompDirectiveFilterFileExt=%[6]d
   111      local shellCompDirectiveFilterDirs=%[7]d
   112      local shellCompDirectiveKeepOrder=%[8]d
   113  
   114      local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder
   115      local -a completions
   116  
   117      __%[1]s_debug "\n========= starting completion logic =========="
   118      __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}"
   119  
   120      # The user could have moved the cursor backwards on the command-line.
   121      # We need to trigger completion from the $CURRENT location, so we need
   122      # to truncate the command-line ($words) up to the $CURRENT location.
   123      # (We cannot use $CURSOR as its value does not work when a command is an alias.)
   124      words=("${=words[1,CURRENT]}")
   125      __%[1]s_debug "Truncated words[*]: ${words[*]},"
   126  
   127      lastParam=${words[-1]}
   128      lastChar=${lastParam[-1]}
   129      __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}"
   130  
   131      # For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>)
   132      # completions must be prefixed with the flag
   133      setopt local_options BASH_REMATCH
   134      if [[ "${lastParam}" =~ '-.*=' ]]; then
   135          # We are dealing with a flag with an =
   136          flagPrefix="-P ${BASH_REMATCH}"
   137      fi
   138  
   139      # Prepare the command to obtain completions
   140      requestComp="${words[1]} %[2]s ${words[2,-1]}"
   141      if [ "${lastChar}" = "" ]; then
   142          # If the last parameter is complete (there is a space following it)
   143          # We add an extra empty parameter so we can indicate this to the go completion code.
   144          __%[1]s_debug "Adding extra empty parameter"
   145          requestComp="${requestComp} \"\""
   146      fi
   147  
   148      __%[1]s_debug "About to call: eval ${requestComp}"
   149  
   150      # Use eval to handle any environment variables and such
   151      out=$(eval ${requestComp} 2>/dev/null)
   152      __%[1]s_debug "completion output: ${out}"
   153  
   154      # Extract the directive integer following a : from the last line
   155      local lastLine
   156      while IFS='\n' read -r line; do
   157          lastLine=${line}
   158      done < <(printf "%%s\n" "${out[@]}")
   159      __%[1]s_debug "last line: ${lastLine}"
   160  
   161      if [ "${lastLine[1]}" = : ]; then
   162          directive=${lastLine[2,-1]}
   163          # Remove the directive including the : and the newline
   164          local suffix
   165          (( suffix=${#lastLine}+2))
   166          out=${out[1,-$suffix]}
   167      else
   168          # There is no directive specified.  Leave $out as is.
   169          __%[1]s_debug "No directive found.  Setting do default"
   170          directive=0
   171      fi
   172  
   173      __%[1]s_debug "directive: ${directive}"
   174      __%[1]s_debug "completions: ${out}"
   175      __%[1]s_debug "flagPrefix: ${flagPrefix}"
   176  
   177      if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
   178          __%[1]s_debug "Completion received error. Ignoring completions."
   179          return
   180      fi
   181  
   182      local activeHelpMarker="%[9]s"
   183      local endIndex=${#activeHelpMarker}
   184      local startIndex=$((${#activeHelpMarker}+1))
   185      local hasActiveHelp=0
   186      while IFS='\n' read -r comp; do
   187          # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
   188          if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
   189              __%[1]s_debug "ActiveHelp found: $comp"
   190              comp="${comp[$startIndex,-1]}"
   191              if [ -n "$comp" ]; then
   192                  compadd -x "${comp}"
   193                  __%[1]s_debug "ActiveHelp will need delimiter"
   194                  hasActiveHelp=1
   195              fi
   196  
   197              continue
   198          fi
   199  
   200          if [ -n "$comp" ]; then
   201              # If requested, completions are returned with a description.
   202              # The description is preceded by a TAB character.
   203              # For zsh's _describe, we need to use a : instead of a TAB.
   204              # We first need to escape any : as part of the completion itself.
   205              comp=${comp//:/\\:}
   206  
   207              local tab="$(printf '\t')"
   208              comp=${comp//$tab/:}
   209  
   210              __%[1]s_debug "Adding completion: ${comp}"
   211              completions+=${comp}
   212              lastComp=$comp
   213          fi
   214      done < <(printf "%%s\n" "${out[@]}")
   215  
   216      # Add a delimiter after the activeHelp statements, but only if:
   217      # - there are completions following the activeHelp statements, or
   218      # - file completion will be performed (so there will be choices after the activeHelp)
   219      if [ $hasActiveHelp -eq 1 ]; then
   220          if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
   221              __%[1]s_debug "Adding activeHelp delimiter"
   222              compadd -x "--"
   223              hasActiveHelp=0
   224          fi
   225      fi
   226  
   227      if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
   228          __%[1]s_debug "Activating nospace."
   229          noSpace="-S ''"
   230      fi
   231  
   232      if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then
   233          __%[1]s_debug "Activating keep order."
   234          keepOrder="-V"
   235      fi
   236  
   237      if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
   238          # File extension filtering
   239          local filteringCmd
   240          filteringCmd='_files'
   241          for filter in ${completions[@]}; do
   242              if [ ${filter[1]} != '*' ]; then
   243                  # zsh requires a glob pattern to do file filtering
   244                  filter="\*.$filter"
   245              fi
   246              filteringCmd+=" -g $filter"
   247          done
   248          filteringCmd+=" ${flagPrefix}"
   249  
   250          __%[1]s_debug "File filtering command: $filteringCmd"
   251          _arguments '*:filename:'"$filteringCmd"
   252      elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
   253          # File completion for directories only
   254          local subdir
   255          subdir="${completions[1]}"
   256          if [ -n "$subdir" ]; then
   257              __%[1]s_debug "Listing directories in $subdir"
   258              pushd "${subdir}" >/dev/null 2>&1
   259          else
   260              __%[1]s_debug "Listing directories in ."
   261          fi
   262  
   263          local result
   264          _arguments '*:dirname:_files -/'" ${flagPrefix}"
   265          result=$?
   266          if [ -n "$subdir" ]; then
   267              popd >/dev/null 2>&1
   268          fi
   269          return $result
   270      else
   271          __%[1]s_debug "Calling _describe"
   272          if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then
   273              __%[1]s_debug "_describe found some completions"
   274  
   275              # Return the success of having called _describe
   276              return 0
   277          else
   278              __%[1]s_debug "_describe did not find completions."
   279              __%[1]s_debug "Checking if we should do file completion."
   280              if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
   281                  __%[1]s_debug "deactivating file completion"
   282  
   283                  # We must return an error code here to let zsh know that there were no
   284                  # completions found by _describe; this is what will trigger other
   285                  # matching algorithms to attempt to find completions.
   286                  # For example zsh can match letters in the middle of words.
   287                  return 1
   288              else
   289                  # Perform file completion
   290                  __%[1]s_debug "Activating file completion"
   291  
   292                  # We must return the result of this command, so it must be the
   293                  # last command, or else we must store its result to return it.
   294                  _arguments '*:filename:_files'" ${flagPrefix}"
   295              fi
   296          fi
   297      fi
   298  }
   299  
   300  # don't run the completion function when being source-ed or eval-ed
   301  if [ "$funcstack[1]" = "_%[1]s" ]; then
   302      _%[1]s
   303  fi
   304  `, name, compCmd,
   305  		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
   306  		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder,
   307  		activeHelpMarker))
   308  }
   309  

View as plain text