bash_completionsV2.go 13 KB

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