zsh_completions.go 11 KB

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