git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cobra/zsh_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  )
    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  
    94  # zsh completion for %-36[1]s -*- shell-script -*-
    95  
    96  __%[1]s_debug()
    97  {
    98      local file="$BASH_COMP_DEBUG_FILE"
    99      if [[ -n ${file} ]]; then
   100          echo "$*" >> "${file}"
   101      fi
   102  }
   103  
   104  _%[1]s()
   105  {
   106      local shellCompDirectiveError=%[3]d
   107      local shellCompDirectiveNoSpace=%[4]d
   108      local shellCompDirectiveNoFileComp=%[5]d
   109      local shellCompDirectiveFilterFileExt=%[6]d
   110      local shellCompDirectiveFilterDirs=%[7]d
   111  
   112      local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace
   113      local -a completions
   114  
   115      __%[1]s_debug "\n========= starting completion logic =========="
   116      __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}"
   117  
   118      # The user could have moved the cursor backwards on the command-line.
   119      # We need to trigger completion from the $CURRENT location, so we need
   120      # to truncate the command-line ($words) up to the $CURRENT location.
   121      # (We cannot use $CURSOR as its value does not work when a command is an alias.)
   122      words=("${=words[1,CURRENT]}")
   123      __%[1]s_debug "Truncated words[*]: ${words[*]},"
   124  
   125      lastParam=${words[-1]}
   126      lastChar=${lastParam[-1]}
   127      __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}"
   128  
   129      # For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>)
   130      # completions must be prefixed with the flag
   131      setopt local_options BASH_REMATCH
   132      if [[ "${lastParam}" =~ '-.*=' ]]; then
   133          # We are dealing with a flag with an =
   134          flagPrefix="-P ${BASH_REMATCH}"
   135      fi
   136  
   137      # Prepare the command to obtain completions
   138      requestComp="${words[1]} %[2]s ${words[2,-1]}"
   139      if [ "${lastChar}" = "" ]; then
   140          # If the last parameter is complete (there is a space following it)
   141          # We add an extra empty parameter so we can indicate this to the go completion code.
   142          __%[1]s_debug "Adding extra empty parameter"
   143          requestComp="${requestComp} \"\""
   144      fi
   145  
   146      __%[1]s_debug "About to call: eval ${requestComp}"
   147  
   148      # Use eval to handle any environment variables and such
   149      out=$(eval ${requestComp} 2>/dev/null)
   150      __%[1]s_debug "completion output: ${out}"
   151  
   152      # Extract the directive integer following a : from the last line
   153      local lastLine
   154      while IFS='\n' read -r line; do
   155          lastLine=${line}
   156      done < <(printf "%%s\n" "${out[@]}")
   157      __%[1]s_debug "last line: ${lastLine}"
   158  
   159      if [ "${lastLine[1]}" = : ]; then
   160          directive=${lastLine[2,-1]}
   161          # Remove the directive including the : and the newline
   162          local suffix
   163          (( suffix=${#lastLine}+2))
   164          out=${out[1,-$suffix]}
   165      else
   166          # There is no directive specified.  Leave $out as is.
   167          __%[1]s_debug "No directive found.  Setting do default"
   168          directive=0
   169      fi
   170  
   171      __%[1]s_debug "directive: ${directive}"
   172      __%[1]s_debug "completions: ${out}"
   173      __%[1]s_debug "flagPrefix: ${flagPrefix}"
   174  
   175      if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
   176          __%[1]s_debug "Completion received error. Ignoring completions."
   177          return
   178      fi
   179  
   180      local activeHelpMarker="%[8]s"
   181      local endIndex=${#activeHelpMarker}
   182      local startIndex=$((${#activeHelpMarker}+1))
   183      local hasActiveHelp=0
   184      while IFS='\n' read -r comp; do
   185          # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
   186          if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
   187              __%[1]s_debug "ActiveHelp found: $comp"
   188              comp="${comp[$startIndex,-1]}"
   189              if [ -n "$comp" ]; then
   190                  compadd -x "${comp}"
   191                  __%[1]s_debug "ActiveHelp will need delimiter"
   192                  hasActiveHelp=1
   193              fi
   194  
   195              continue
   196          fi
   197  
   198          if [ -n "$comp" ]; then
   199              # If requested, completions are returned with a description.
   200              # The description is preceded by a TAB character.
   201              # For zsh's _describe, we need to use a : instead of a TAB.
   202              # We first need to escape any : as part of the completion itself.
   203              comp=${comp//:/\\:}
   204  
   205              local tab="$(printf '\t')"
   206              comp=${comp//$tab/:}
   207  
   208              __%[1]s_debug "Adding completion: ${comp}"
   209              completions+=${comp}
   210              lastComp=$comp
   211          fi
   212      done < <(printf "%%s\n" "${out[@]}")
   213  
   214      # Add a delimiter after the activeHelp statements, but only if:
   215      # - there are completions following the activeHelp statements, or
   216      # - file completion will be performed (so there will be choices after the activeHelp)
   217      if [ $hasActiveHelp -eq 1 ]; then
   218          if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
   219              __%[1]s_debug "Adding activeHelp delimiter"
   220              compadd -x "--"
   221              hasActiveHelp=0
   222          fi
   223      fi
   224  
   225      if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
   226          __%[1]s_debug "Activating nospace."
   227          noSpace="-S ''"
   228      fi
   229  
   230      if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
   231          # File extension filtering
   232          local filteringCmd
   233          filteringCmd='_files'
   234          for filter in ${completions[@]}; do
   235              if [ ${filter[1]} != '*' ]; then
   236                  # zsh requires a glob pattern to do file filtering
   237                  filter="\*.$filter"
   238              fi
   239              filteringCmd+=" -g $filter"
   240          done
   241          filteringCmd+=" ${flagPrefix}"
   242  
   243          __%[1]s_debug "File filtering command: $filteringCmd"
   244          _arguments '*:filename:'"$filteringCmd"
   245      elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
   246          # File completion for directories only
   247          local subdir
   248          subdir="${completions[1]}"
   249          if [ -n "$subdir" ]; then
   250              __%[1]s_debug "Listing directories in $subdir"
   251              pushd "${subdir}" >/dev/null 2>&1
   252          else
   253              __%[1]s_debug "Listing directories in ."
   254          fi
   255  
   256          local result
   257          _arguments '*:dirname:_files -/'" ${flagPrefix}"
   258          result=$?
   259          if [ -n "$subdir" ]; then
   260              popd >/dev/null 2>&1
   261          fi
   262          return $result
   263      else
   264          __%[1]s_debug "Calling _describe"
   265          if eval _describe "completions" completions $flagPrefix $noSpace; then
   266              __%[1]s_debug "_describe found some completions"
   267  
   268              # Return the success of having called _describe
   269              return 0
   270          else
   271              __%[1]s_debug "_describe did not find completions."
   272              __%[1]s_debug "Checking if we should do file completion."
   273              if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
   274                  __%[1]s_debug "deactivating file completion"
   275  
   276                  # We must return an error code here to let zsh know that there were no
   277                  # completions found by _describe; this is what will trigger other
   278                  # matching algorithms to attempt to find completions.
   279                  # For example zsh can match letters in the middle of words.
   280                  return 1
   281              else
   282                  # Perform file completion
   283                  __%[1]s_debug "Activating file completion"
   284  
   285                  # We must return the result of this command, so it must be the
   286                  # last command, or else we must store its result to return it.
   287                  _arguments '*:filename:_files'" ${flagPrefix}"
   288              fi
   289          fi
   290      fi
   291  }
   292  
   293  # don't run the completion function when being source-ed or eval-ed
   294  if [ "$funcstack[1]" = "_%[1]s" ]; then
   295      _%[1]s
   296  fi
   297  `, name, compCmd,
   298  		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
   299  		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs,
   300  		activeHelpMarker))
   301  }