bash_completionsV2.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package cobra
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "os"
  7. )
  8. func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error {
  9. buf := new(bytes.Buffer)
  10. genBashComp(buf, c.Name(), includeDesc)
  11. _, err := buf.WriteTo(w)
  12. return err
  13. }
  14. func genBashComp(buf io.StringWriter, name string, includeDesc bool) {
  15. compCmd := ShellCompRequestCmd
  16. if !includeDesc {
  17. compCmd = ShellCompNoDescRequestCmd
  18. }
  19. WriteStringAndCheck(buf, fmt.Sprintf(`# bash completion V2 for %-36[1]s -*- shell-script -*-
  20. __%[1]s_debug()
  21. {
  22. if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
  23. echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
  24. fi
  25. }
  26. # Macs have bash3 for which the bash-completion package doesn't include
  27. # _init_completion. This is a minimal version of that function.
  28. __%[1]s_init_completion()
  29. {
  30. COMPREPLY=()
  31. _get_comp_words_by_ref "$@" cur prev words cword
  32. }
  33. # This function calls the %[1]s program to obtain the completion
  34. # results and the directive. It fills the 'out' and 'directive' vars.
  35. __%[1]s_get_completion_results() {
  36. local requestComp lastParam lastChar args
  37. # Prepare the command to request completions for the program.
  38. # Calling ${words[0]} instead of directly %[1]s allows to handle aliases
  39. args=("${words[@]:1}")
  40. requestComp="${words[0]} %[2]s ${args[*]}"
  41. lastParam=${words[$((${#words[@]}-1))]}
  42. lastChar=${lastParam:$((${#lastParam}-1)):1}
  43. __%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}"
  44. if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
  45. # If the last parameter is complete (there is a space following it)
  46. # We add an extra empty parameter so we can indicate this to the go method.
  47. __%[1]s_debug "Adding extra empty parameter"
  48. requestComp="${requestComp} ''"
  49. fi
  50. # When completing a flag with an = (e.g., %[1]s -n=<TAB>)
  51. # bash focuses on the part after the =, so we need to remove
  52. # the flag part from $cur
  53. if [[ "${cur}" == -*=* ]]; then
  54. cur="${cur#*=}"
  55. fi
  56. __%[1]s_debug "Calling ${requestComp}"
  57. # Use eval to handle any environment variables and such
  58. out=$(eval "${requestComp}" 2>/dev/null)
  59. # Extract the directive integer at the very end of the output following a colon (:)
  60. directive=${out##*:}
  61. # Remove the directive
  62. out=${out%%:*}
  63. if [ "${directive}" = "${out}" ]; then
  64. # There is not directive specified
  65. directive=0
  66. fi
  67. __%[1]s_debug "The completion directive is: ${directive}"
  68. __%[1]s_debug "The completions are: ${out[*]}"
  69. }
  70. __%[1]s_process_completion_results() {
  71. local shellCompDirectiveError=%[3]d
  72. local shellCompDirectiveNoSpace=%[4]d
  73. local shellCompDirectiveNoFileComp=%[5]d
  74. local shellCompDirectiveFilterFileExt=%[6]d
  75. local shellCompDirectiveFilterDirs=%[7]d
  76. if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
  77. # Error code. No completion.
  78. __%[1]s_debug "Received error from custom completion go code"
  79. return
  80. else
  81. if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
  82. if [[ $(type -t compopt) = "builtin" ]]; then
  83. __%[1]s_debug "Activating no space"
  84. compopt -o nospace
  85. else
  86. __%[1]s_debug "No space directive not supported in this version of bash"
  87. fi
  88. fi
  89. if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
  90. if [[ $(type -t compopt) = "builtin" ]]; then
  91. __%[1]s_debug "Activating no file completion"
  92. compopt +o default
  93. else
  94. __%[1]s_debug "No file completion directive not supported in this version of bash"
  95. fi
  96. fi
  97. fi
  98. if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
  99. # File extension filtering
  100. local fullFilter filter filteringCmd
  101. # Do not use quotes around the $out variable or else newline
  102. # characters will be kept.
  103. for filter in ${out[*]}; do
  104. fullFilter+="$filter|"
  105. done
  106. filteringCmd="_filedir $fullFilter"
  107. __%[1]s_debug "File filtering command: $filteringCmd"
  108. $filteringCmd
  109. elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
  110. # File completion for directories only
  111. # Use printf to strip any trailing newline
  112. local subdir
  113. subdir=$(printf "%%s" "${out[0]}")
  114. if [ -n "$subdir" ]; then
  115. __%[1]s_debug "Listing directories in $subdir"
  116. pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
  117. else
  118. __%[1]s_debug "Listing directories in ."
  119. _filedir -d
  120. fi
  121. else
  122. __%[1]s_handle_standard_completion_case
  123. fi
  124. __%[1]s_handle_special_char "$cur" :
  125. __%[1]s_handle_special_char "$cur" =
  126. }
  127. __%[1]s_handle_standard_completion_case() {
  128. local tab comp
  129. tab=$(printf '\t')
  130. local longest=0
  131. # Look for the longest completion so that we can format things nicely
  132. while IFS='' read -r comp; do
  133. # Strip any description before checking the length
  134. comp=${comp%%%%$tab*}
  135. # Only consider the completions that match
  136. comp=$(compgen -W "$comp" -- "$cur")
  137. if ((${#comp}>longest)); then
  138. longest=${#comp}
  139. fi
  140. done < <(printf "%%s\n" "${out[@]}")
  141. local completions=()
  142. while IFS='' read -r comp; do
  143. if [ -z "$comp" ]; then
  144. continue
  145. fi
  146. __%[1]s_debug "Original comp: $comp"
  147. comp="$(__%[1]s_format_comp_descriptions "$comp" "$longest")"
  148. __%[1]s_debug "Final comp: $comp"
  149. completions+=("$comp")
  150. done < <(printf "%%s\n" "${out[@]}")
  151. while IFS='' read -r comp; do
  152. COMPREPLY+=("$comp")
  153. done < <(compgen -W "${completions[*]}" -- "$cur")
  154. # If there is a single completion left, remove the description text
  155. if [ ${#COMPREPLY[*]} -eq 1 ]; then
  156. __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
  157. comp="${COMPREPLY[0]%%%% *}"
  158. __%[1]s_debug "Removed description from single completion, which is now: ${comp}"
  159. COMPREPLY=()
  160. COMPREPLY+=("$comp")
  161. fi
  162. }
  163. __%[1]s_handle_special_char()
  164. {
  165. local comp="$1"
  166. local char=$2
  167. if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
  168. local word=${comp%%"${comp##*${char}}"}
  169. local idx=${#COMPREPLY[*]}
  170. while [[ $((--idx)) -ge 0 ]]; do
  171. COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"}
  172. done
  173. fi
  174. }
  175. __%[1]s_format_comp_descriptions()
  176. {
  177. local tab
  178. tab=$(printf '\t')
  179. local comp="$1"
  180. local longest=$2
  181. # Properly format the description string which follows a tab character if there is one
  182. if [[ "$comp" == *$tab* ]]; then
  183. desc=${comp#*$tab}
  184. comp=${comp%%%%$tab*}
  185. # $COLUMNS stores the current shell width.
  186. # Remove an extra 4 because we add 2 spaces and 2 parentheses.
  187. maxdesclength=$(( COLUMNS - longest - 4 ))
  188. # Make sure we can fit a description of at least 8 characters
  189. # if we are to align the descriptions.
  190. if [[ $maxdesclength -gt 8 ]]; then
  191. # Add the proper number of spaces to align the descriptions
  192. for ((i = ${#comp} ; i < longest ; i++)); do
  193. comp+=" "
  194. done
  195. else
  196. # Don't pad the descriptions so we can fit more text after the completion
  197. maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
  198. fi
  199. # If there is enough space for any description text,
  200. # truncate the descriptions that are too long for the shell width
  201. if [ $maxdesclength -gt 0 ]; then
  202. if [ ${#desc} -gt $maxdesclength ]; then
  203. desc=${desc:0:$(( maxdesclength - 1 ))}
  204. desc+="…"
  205. fi
  206. comp+=" ($desc)"
  207. fi
  208. fi
  209. # Must use printf to escape all special characters
  210. printf "%%q" "${comp}"
  211. }
  212. __start_%[1]s()
  213. {
  214. local cur prev words cword split
  215. COMPREPLY=()
  216. # Call _init_completion from the bash-completion package
  217. # to prepare the arguments properly
  218. if declare -F _init_completion >/dev/null 2>&1; then
  219. _init_completion -n "=:" || return
  220. else
  221. __%[1]s_init_completion -n "=:" || return
  222. fi
  223. __%[1]s_debug
  224. __%[1]s_debug "========= starting completion logic =========="
  225. __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
  226. # The user could have moved the cursor backwards on the command-line.
  227. # We need to trigger completion from the $cword location, so we need
  228. # to truncate the command-line ($words) up to the $cword location.
  229. words=("${words[@]:0:$cword+1}")
  230. __%[1]s_debug "Truncated words[*]: ${words[*]},"
  231. local out directive
  232. __%[1]s_get_completion_results
  233. __%[1]s_process_completion_results
  234. }
  235. if [[ $(type -t compopt) = "builtin" ]]; then
  236. complete -o default -F __start_%[1]s %[1]s
  237. else
  238. complete -o default -o nospace -F __start_%[1]s %[1]s
  239. fi
  240. # ex: ts=4 sw=4 et filetype=sh
  241. `, name, compCmd,
  242. ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
  243. ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs))
  244. }
  245. // GenBashCompletionFileV2 generates Bash completion version 2.
  246. func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error {
  247. outFile, err := os.Create(filename)
  248. if err != nil {
  249. return err
  250. }
  251. defer outFile.Close()
  252. return c.GenBashCompletionV2(outFile, includeDesc)
  253. }
  254. // GenBashCompletionV2 generates Bash completion file version 2
  255. // and writes it to the passed writer.
  256. func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error {
  257. return c.genBashCompletion(w, includeDesc)
  258. }