bash_completionsV2.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  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. __%[1]s_handle_activeHelp
  158. }
  159. __%[1]s_handle_activeHelp() {
  160. # Print the activeHelp statements
  161. if ((${#activeHelp[*]} != 0)); then
  162. if [ -z $COMP_TYPE ]; then
  163. # Bash v3 does not set the COMP_TYPE variable.
  164. printf "\n";
  165. printf "%%s\n" "${activeHelp[@]}"
  166. printf "\n"
  167. __%[1]s_reprint_commandLine
  168. return
  169. fi
  170. # Only print ActiveHelp on the second TAB press
  171. if [ $COMP_TYPE -eq 63 ]; then
  172. printf "\n"
  173. printf "%%s\n" "${activeHelp[@]}"
  174. if ((${#COMPREPLY[*]} == 0)); then
  175. # When there are no completion choices from the program, file completion
  176. # may kick in if the program has not disabled it; in such a case, we want
  177. # to know if any files will match what the user typed, so that we know if
  178. # there will be completions presented, so that we know how to handle ActiveHelp.
  179. # To find out, we actually trigger the file completion ourselves;
  180. # the call to _filedir will fill COMPREPLY if files match.
  181. if (((directive & shellCompDirectiveNoFileComp) == 0)); then
  182. __%[1]s_debug "Listing files"
  183. _filedir
  184. fi
  185. fi
  186. if ((${#COMPREPLY[*]} != 0)); then
  187. # If there are completion choices to be shown, print a delimiter.
  188. # Re-printing the command-line will automatically be done
  189. # by the shell when it prints the completion choices.
  190. printf -- "--"
  191. else
  192. # When there are no completion choices at all, we need
  193. # to re-print the command-line since the shell will
  194. # not be doing it itself.
  195. __%[1]s_reprint_commandLine
  196. fi
  197. elif [ $COMP_TYPE -eq 37 ] || [ $COMP_TYPE -eq 42 ]; then
  198. # For completion type: menu-complete/menu-complete-backward and insert-completions
  199. # the completions are immediately inserted into the command-line, so we first
  200. # print the activeHelp message and reprint the command-line since the shell won't.
  201. printf "\n"
  202. printf "%%s\n" "${activeHelp[@]}"
  203. __%[1]s_reprint_commandLine
  204. fi
  205. fi
  206. }
  207. __%[1]s_reprint_commandLine() {
  208. # The prompt format is only available from bash 4.4.
  209. # We test if it is available before using it.
  210. if (x=${PS1@P}) 2> /dev/null; then
  211. printf "%%s" "${PS1@P}${COMP_LINE[@]}"
  212. else
  213. # Can't print the prompt. Just print the
  214. # text the user had typed, it is workable enough.
  215. printf "%%s" "${COMP_LINE[@]}"
  216. fi
  217. }
  218. # Separate activeHelp lines from real completions.
  219. # Fills the $activeHelp and $completions arrays.
  220. __%[1]s_extract_activeHelp() {
  221. local activeHelpMarker="%[9]s"
  222. local endIndex=${#activeHelpMarker}
  223. while IFS='' read -r comp; do
  224. [[ -z $comp ]] && continue
  225. if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then
  226. comp=${comp:endIndex}
  227. __%[1]s_debug "ActiveHelp found: $comp"
  228. if [[ -n $comp ]]; then
  229. activeHelp+=("$comp")
  230. fi
  231. else
  232. # Not an activeHelp line but a normal completion
  233. completions+=("$comp")
  234. fi
  235. done <<<"${out}"
  236. }
  237. __%[1]s_handle_completion_types() {
  238. __%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE"
  239. case $COMP_TYPE in
  240. 37|42)
  241. # Type: menu-complete/menu-complete-backward and insert-completions
  242. # If the user requested inserting one completion at a time, or all
  243. # completions at once on the command-line we must remove the descriptions.
  244. # https://github.com/spf13/cobra/issues/1508
  245. # If there are no completions, we don't need to do anything
  246. (( ${#completions[@]} == 0 )) && return 0
  247. local tab=$'\t'
  248. # Strip any description and escape the completion to handled special characters
  249. IFS=$'\n' read -ra completions -d '' < <(printf "%%q\n" "${completions[@]%%%%$tab*}")
  250. # Only consider the completions that match
  251. IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "${cur}")
  252. # compgen looses the escaping so we need to escape all completions again since they will
  253. # all be inserted on the command-line.
  254. IFS=$'\n' read -ra COMPREPLY -d '' < <(printf "%%q\n" "${COMPREPLY[@]}")
  255. ;;
  256. *)
  257. # Type: complete (normal completion)
  258. __%[1]s_handle_standard_completion_case
  259. ;;
  260. esac
  261. }
  262. __%[1]s_handle_standard_completion_case() {
  263. local tab=$'\t'
  264. # If there are no completions, we don't need to do anything
  265. (( ${#completions[@]} == 0 )) && return 0
  266. # Short circuit to optimize if we don't have descriptions
  267. if [[ "${completions[*]}" != *$tab* ]]; then
  268. # First, escape the completions to handle special characters
  269. IFS=$'\n' read -ra completions -d '' < <(printf "%%q\n" "${completions[@]}")
  270. # Only consider the completions that match what the user typed
  271. IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "${cur}")
  272. # compgen looses the escaping so, if there is only a single completion, we need to
  273. # escape it again because it will be inserted on the command-line. If there are multiple
  274. # completions, we don't want to escape them because they will be printed in a list
  275. # and we don't want to show escape characters in that list.
  276. if (( ${#COMPREPLY[@]} == 1 )); then
  277. COMPREPLY[0]=$(printf "%%q" "${COMPREPLY[0]}")
  278. fi
  279. return 0
  280. fi
  281. local longest=0
  282. local compline
  283. # Look for the longest completion so that we can format things nicely
  284. while IFS='' read -r compline; do
  285. [[ -z $compline ]] && continue
  286. # Before checking if the completion matches what the user typed,
  287. # we need to strip any description and escape the completion to handle special
  288. # characters because those escape characters are part of what the user typed.
  289. # Don't call "printf" in a sub-shell because it will be much slower
  290. # since we are in a loop.
  291. printf -v comp "%%q" "${compline%%%%$tab*}" &>/dev/null || comp=$(printf "%%q" "${compline%%%%$tab*}")
  292. # Only consider the completions that match
  293. [[ $comp == "$cur"* ]] || continue
  294. # The completions matches. Add it to the list of full completions including
  295. # its description. We don't escape the completion because it may get printed
  296. # in a list if there are more than one and we don't want show escape characters
  297. # in that list.
  298. COMPREPLY+=("$compline")
  299. # Strip any description before checking the length, and again, don't escape
  300. # the completion because this length is only used when printing the completions
  301. # in a list and we don't want show escape characters in that list.
  302. comp=${compline%%%%$tab*}
  303. if ((${#comp}>longest)); then
  304. longest=${#comp}
  305. fi
  306. done < <(printf "%%s\n" "${completions[@]}")
  307. # If there is a single completion left, remove the description text and escape any special characters
  308. if ((${#COMPREPLY[*]} == 1)); then
  309. __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
  310. COMPREPLY[0]=$(printf "%%q" "${COMPREPLY[0]%%%%$tab*}")
  311. __%[1]s_debug "Removed description from single completion, which is now: ${COMPREPLY[0]}"
  312. else
  313. # Format the descriptions
  314. __%[1]s_format_comp_descriptions $longest
  315. fi
  316. }
  317. __%[1]s_handle_special_char()
  318. {
  319. local comp="$1"
  320. local char=$2
  321. if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
  322. local word=${comp%%"${comp##*${char}}"}
  323. local idx=${#COMPREPLY[*]}
  324. while ((--idx >= 0)); do
  325. COMPREPLY[idx]=${COMPREPLY[idx]#"$word"}
  326. done
  327. fi
  328. }
  329. __%[1]s_format_comp_descriptions()
  330. {
  331. local tab=$'\t'
  332. local comp desc maxdesclength
  333. local longest=$1
  334. local i ci
  335. for ci in ${!COMPREPLY[*]}; do
  336. comp=${COMPREPLY[ci]}
  337. # Properly format the description string which follows a tab character if there is one
  338. if [[ "$comp" == *$tab* ]]; then
  339. __%[1]s_debug "Original comp: $comp"
  340. desc=${comp#*$tab}
  341. comp=${comp%%%%$tab*}
  342. # $COLUMNS stores the current shell width.
  343. # Remove an extra 4 because we add 2 spaces and 2 parentheses.
  344. maxdesclength=$(( COLUMNS - longest - 4 ))
  345. # Make sure we can fit a description of at least 8 characters
  346. # if we are to align the descriptions.
  347. if ((maxdesclength > 8)); then
  348. # Add the proper number of spaces to align the descriptions
  349. for ((i = ${#comp} ; i < longest ; i++)); do
  350. comp+=" "
  351. done
  352. else
  353. # Don't pad the descriptions so we can fit more text after the completion
  354. maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
  355. fi
  356. # If there is enough space for any description text,
  357. # truncate the descriptions that are too long for the shell width
  358. if ((maxdesclength > 0)); then
  359. if ((${#desc} > maxdesclength)); then
  360. desc=${desc:0:$(( maxdesclength - 1 ))}
  361. desc+="…"
  362. fi
  363. comp+=" ($desc)"
  364. fi
  365. COMPREPLY[ci]=$comp
  366. __%[1]s_debug "Final comp: $comp"
  367. fi
  368. done
  369. }
  370. __start_%[1]s()
  371. {
  372. local cur prev words cword split
  373. COMPREPLY=()
  374. # Call _init_completion from the bash-completion package
  375. # to prepare the arguments properly
  376. if declare -F _init_completion >/dev/null 2>&1; then
  377. _init_completion -n =: || return
  378. else
  379. __%[1]s_init_completion -n =: || return
  380. fi
  381. __%[1]s_debug
  382. __%[1]s_debug "========= starting completion logic =========="
  383. __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
  384. # The user could have moved the cursor backwards on the command-line.
  385. # We need to trigger completion from the $cword location, so we need
  386. # to truncate the command-line ($words) up to the $cword location.
  387. words=("${words[@]:0:$cword+1}")
  388. __%[1]s_debug "Truncated words[*]: ${words[*]},"
  389. local out directive
  390. __%[1]s_get_completion_results
  391. __%[1]s_process_completion_results
  392. }
  393. if [[ $(type -t compopt) = "builtin" ]]; then
  394. complete -o default -F __start_%[1]s %[1]s
  395. else
  396. complete -o default -o nospace -F __start_%[1]s %[1]s
  397. fi
  398. # ex: ts=4 sw=4 et filetype=sh
  399. `, name, compCmd,
  400. ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
  401. ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder,
  402. activeHelpMarker))
  403. }
  404. // GenBashCompletionFileV2 generates Bash completion version 2.
  405. func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error {
  406. outFile, err := os.Create(filename)
  407. if err != nil {
  408. return err
  409. }
  410. defer outFile.Close()
  411. return c.GenBashCompletionV2(outFile, includeDesc)
  412. }
  413. // GenBashCompletionV2 generates Bash completion file version 2
  414. // and writes it to the passed writer.
  415. func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error {
  416. return c.genBashCompletion(w, includeDesc)
  417. }