bash_completions.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. package cobra
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "os"
  7. "sort"
  8. "strings"
  9. "github.com/spf13/pflag"
  10. )
  11. // Annotations for Bash completion.
  12. const (
  13. BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions"
  14. BashCompCustom = "cobra_annotation_bash_completion_custom"
  15. BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag"
  16. BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir"
  17. )
  18. func writePreamble(buf io.StringWriter, name string) {
  19. WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
  20. WriteStringAndCheck(buf, fmt.Sprintf(`
  21. __%[1]s_debug()
  22. {
  23. if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then
  24. echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
  25. fi
  26. }
  27. # Homebrew on Macs have version 1.3 of bash-completion which doesn't include
  28. # _init_completion. This is a very minimal version of that function.
  29. __%[1]s_init_completion()
  30. {
  31. COMPREPLY=()
  32. _get_comp_words_by_ref "$@" cur prev words cword
  33. }
  34. __%[1]s_index_of_word()
  35. {
  36. local w word=$1
  37. shift
  38. index=0
  39. for w in "$@"; do
  40. [[ $w = "$word" ]] && return
  41. index=$((index+1))
  42. done
  43. index=-1
  44. }
  45. __%[1]s_contains_word()
  46. {
  47. local w word=$1; shift
  48. for w in "$@"; do
  49. [[ $w = "$word" ]] && return
  50. done
  51. return 1
  52. }
  53. __%[1]s_handle_go_custom_completion()
  54. {
  55. __%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
  56. local shellCompDirectiveError=%[3]d
  57. local shellCompDirectiveNoSpace=%[4]d
  58. local shellCompDirectiveNoFileComp=%[5]d
  59. local shellCompDirectiveFilterFileExt=%[6]d
  60. local shellCompDirectiveFilterDirs=%[7]d
  61. local out requestComp lastParam lastChar comp directive args
  62. # Prepare the command to request completions for the program.
  63. # Calling ${words[0]} instead of directly %[1]s allows to handle aliases
  64. args=("${words[@]:1}")
  65. requestComp="${words[0]} %[2]s ${args[*]}"
  66. lastParam=${words[$((${#words[@]}-1))]}
  67. lastChar=${lastParam:$((${#lastParam}-1)):1}
  68. __%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
  69. if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
  70. # If the last parameter is complete (there is a space following it)
  71. # We add an extra empty parameter so we can indicate this to the go method.
  72. __%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter"
  73. requestComp="${requestComp} \"\""
  74. fi
  75. __%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}"
  76. # Use eval to handle any environment variables and such
  77. out=$(eval "${requestComp}" 2>/dev/null)
  78. # Extract the directive integer at the very end of the output following a colon (:)
  79. directive=${out##*:}
  80. # Remove the directive
  81. out=${out%%:*}
  82. if [ "${directive}" = "${out}" ]; then
  83. # There is not directive specified
  84. directive=0
  85. fi
  86. __%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
  87. __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
  88. if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
  89. # Error code. No completion.
  90. __%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code"
  91. return
  92. else
  93. if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
  94. if [[ $(type -t compopt) = "builtin" ]]; then
  95. __%[1]s_debug "${FUNCNAME[0]}: activating no space"
  96. compopt -o nospace
  97. fi
  98. fi
  99. if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
  100. if [[ $(type -t compopt) = "builtin" ]]; then
  101. __%[1]s_debug "${FUNCNAME[0]}: activating no file completion"
  102. compopt +o default
  103. fi
  104. fi
  105. fi
  106. if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
  107. # File extension filtering
  108. local fullFilter filter filteringCmd
  109. # Do not use quotes around the $out variable or else newline
  110. # characters will be kept.
  111. for filter in ${out[*]}; do
  112. fullFilter+="$filter|"
  113. done
  114. filteringCmd="_filedir $fullFilter"
  115. __%[1]s_debug "File filtering command: $filteringCmd"
  116. $filteringCmd
  117. elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
  118. # File completion for directories only
  119. local subDir
  120. # Use printf to strip any trailing newline
  121. subdir=$(printf "%%s" "${out[0]}")
  122. if [ -n "$subdir" ]; then
  123. __%[1]s_debug "Listing directories in $subdir"
  124. __%[1]s_handle_subdirs_in_dir_flag "$subdir"
  125. else
  126. __%[1]s_debug "Listing directories in ."
  127. _filedir -d
  128. fi
  129. else
  130. while IFS='' read -r comp; do
  131. COMPREPLY+=("$comp")
  132. done < <(compgen -W "${out[*]}" -- "$cur")
  133. fi
  134. }
  135. __%[1]s_handle_reply()
  136. {
  137. __%[1]s_debug "${FUNCNAME[0]}"
  138. local comp
  139. case $cur in
  140. -*)
  141. if [[ $(type -t compopt) = "builtin" ]]; then
  142. compopt -o nospace
  143. fi
  144. local allflags
  145. if [ ${#must_have_one_flag[@]} -ne 0 ]; then
  146. allflags=("${must_have_one_flag[@]}")
  147. else
  148. allflags=("${flags[*]} ${two_word_flags[*]}")
  149. fi
  150. while IFS='' read -r comp; do
  151. COMPREPLY+=("$comp")
  152. done < <(compgen -W "${allflags[*]}" -- "$cur")
  153. if [[ $(type -t compopt) = "builtin" ]]; then
  154. [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
  155. fi
  156. # complete after --flag=abc
  157. if [[ $cur == *=* ]]; then
  158. if [[ $(type -t compopt) = "builtin" ]]; then
  159. compopt +o nospace
  160. fi
  161. local index flag
  162. flag="${cur%%=*}"
  163. __%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}"
  164. COMPREPLY=()
  165. if [[ ${index} -ge 0 ]]; then
  166. PREFIX=""
  167. cur="${cur#*=}"
  168. ${flags_completion[${index}]}
  169. if [ -n "${ZSH_VERSION}" ]; then
  170. # zsh completion needs --flag= prefix
  171. eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
  172. fi
  173. fi
  174. fi
  175. return 0;
  176. ;;
  177. esac
  178. # check if we are handling a flag with special work handling
  179. local index
  180. __%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}"
  181. if [[ ${index} -ge 0 ]]; then
  182. ${flags_completion[${index}]}
  183. return
  184. fi
  185. # we are parsing a flag and don't have a special handler, no completion
  186. if [[ ${cur} != "${words[cword]}" ]]; then
  187. return
  188. fi
  189. local completions
  190. completions=("${commands[@]}")
  191. if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
  192. completions+=("${must_have_one_noun[@]}")
  193. elif [[ -n "${has_completion_function}" ]]; then
  194. # if a go completion function is provided, defer to that function
  195. __%[1]s_handle_go_custom_completion
  196. fi
  197. if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
  198. completions+=("${must_have_one_flag[@]}")
  199. fi
  200. while IFS='' read -r comp; do
  201. COMPREPLY+=("$comp")
  202. done < <(compgen -W "${completions[*]}" -- "$cur")
  203. if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
  204. while IFS='' read -r comp; do
  205. COMPREPLY+=("$comp")
  206. done < <(compgen -W "${noun_aliases[*]}" -- "$cur")
  207. fi
  208. if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
  209. if declare -F __%[1]s_custom_func >/dev/null; then
  210. # try command name qualified custom func
  211. __%[1]s_custom_func
  212. else
  213. # otherwise fall back to unqualified for compatibility
  214. declare -F __custom_func >/dev/null && __custom_func
  215. fi
  216. fi
  217. # available in bash-completion >= 2, not always present on macOS
  218. if declare -F __ltrim_colon_completions >/dev/null; then
  219. __ltrim_colon_completions "$cur"
  220. fi
  221. # If there is only 1 completion and it is a flag with an = it will be completed
  222. # but we don't want a space after the =
  223. if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
  224. compopt -o nospace
  225. fi
  226. }
  227. # The arguments should be in the form "ext1|ext2|extn"
  228. __%[1]s_handle_filename_extension_flag()
  229. {
  230. local ext="$1"
  231. _filedir "@(${ext})"
  232. }
  233. __%[1]s_handle_subdirs_in_dir_flag()
  234. {
  235. local dir="$1"
  236. pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
  237. }
  238. __%[1]s_handle_flag()
  239. {
  240. __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  241. # if a command required a flag, and we found it, unset must_have_one_flag()
  242. local flagname=${words[c]}
  243. local flagvalue
  244. # if the word contained an =
  245. if [[ ${words[c]} == *"="* ]]; then
  246. flagvalue=${flagname#*=} # take in as flagvalue after the =
  247. flagname=${flagname%%=*} # strip everything after the =
  248. flagname="${flagname}=" # but put the = back
  249. fi
  250. __%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}"
  251. if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
  252. must_have_one_flag=()
  253. fi
  254. # if you set a flag which only applies to this command, don't show subcommands
  255. if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
  256. commands=()
  257. fi
  258. # keep flag value with flagname as flaghash
  259. # flaghash variable is an associative array which is only supported in bash > 3.
  260. if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then
  261. if [ -n "${flagvalue}" ] ; then
  262. flaghash[${flagname}]=${flagvalue}
  263. elif [ -n "${words[ $((c+1)) ]}" ] ; then
  264. flaghash[${flagname}]=${words[ $((c+1)) ]}
  265. else
  266. flaghash[${flagname}]="true" # pad "true" for bool flag
  267. fi
  268. fi
  269. # skip the argument to a two word flag
  270. if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then
  271. __%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
  272. c=$((c+1))
  273. # if we are looking for a flags value, don't show commands
  274. if [[ $c -eq $cword ]]; then
  275. commands=()
  276. fi
  277. fi
  278. c=$((c+1))
  279. }
  280. __%[1]s_handle_noun()
  281. {
  282. __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  283. if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
  284. must_have_one_noun=()
  285. elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then
  286. must_have_one_noun=()
  287. fi
  288. nouns+=("${words[c]}")
  289. c=$((c+1))
  290. }
  291. __%[1]s_handle_command()
  292. {
  293. __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  294. local next_command
  295. if [[ -n ${last_command} ]]; then
  296. next_command="_${last_command}_${words[c]//:/__}"
  297. else
  298. if [[ $c -eq 0 ]]; then
  299. next_command="_%[1]s_root_command"
  300. else
  301. next_command="_${words[c]//:/__}"
  302. fi
  303. fi
  304. c=$((c+1))
  305. __%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}"
  306. declare -F "$next_command" >/dev/null && $next_command
  307. }
  308. __%[1]s_handle_word()
  309. {
  310. if [[ $c -ge $cword ]]; then
  311. __%[1]s_handle_reply
  312. return
  313. fi
  314. __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  315. if [[ "${words[c]}" == -* ]]; then
  316. __%[1]s_handle_flag
  317. elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then
  318. __%[1]s_handle_command
  319. elif [[ $c -eq 0 ]]; then
  320. __%[1]s_handle_command
  321. elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then
  322. # aliashash variable is an associative array which is only supported in bash > 3.
  323. if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then
  324. words[c]=${aliashash[${words[c]}]}
  325. __%[1]s_handle_command
  326. else
  327. __%[1]s_handle_noun
  328. fi
  329. else
  330. __%[1]s_handle_noun
  331. fi
  332. __%[1]s_handle_word
  333. }
  334. `, name, ShellCompNoDescRequestCmd,
  335. ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
  336. ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs))
  337. }
  338. func writePostscript(buf io.StringWriter, name string) {
  339. name = strings.Replace(name, ":", "__", -1)
  340. WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name))
  341. WriteStringAndCheck(buf, fmt.Sprintf(`{
  342. local cur prev words cword split
  343. declare -A flaghash 2>/dev/null || :
  344. declare -A aliashash 2>/dev/null || :
  345. if declare -F _init_completion >/dev/null 2>&1; then
  346. _init_completion -s || return
  347. else
  348. __%[1]s_init_completion -n "=" || return
  349. fi
  350. local c=0
  351. local flags=()
  352. local two_word_flags=()
  353. local local_nonpersistent_flags=()
  354. local flags_with_completion=()
  355. local flags_completion=()
  356. local commands=("%[1]s")
  357. local command_aliases=()
  358. local must_have_one_flag=()
  359. local must_have_one_noun=()
  360. local has_completion_function
  361. local last_command
  362. local nouns=()
  363. local noun_aliases=()
  364. __%[1]s_handle_word
  365. }
  366. `, name))
  367. WriteStringAndCheck(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then
  368. complete -o default -F __start_%s %s
  369. else
  370. complete -o default -o nospace -F __start_%s %s
  371. fi
  372. `, name, name, name, name))
  373. WriteStringAndCheck(buf, "# ex: ts=4 sw=4 et filetype=sh\n")
  374. }
  375. func writeCommands(buf io.StringWriter, cmd *Command) {
  376. WriteStringAndCheck(buf, " commands=()\n")
  377. for _, c := range cmd.Commands() {
  378. if !c.IsAvailableCommand() && c != cmd.helpCommand {
  379. continue
  380. }
  381. WriteStringAndCheck(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name()))
  382. writeCmdAliases(buf, c)
  383. }
  384. WriteStringAndCheck(buf, "\n")
  385. }
  386. func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *Command) {
  387. for key, value := range annotations {
  388. switch key {
  389. case BashCompFilenameExt:
  390. WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
  391. var ext string
  392. if len(value) > 0 {
  393. ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Root().Name()) + strings.Join(value, "|")
  394. } else {
  395. ext = "_filedir"
  396. }
  397. WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
  398. case BashCompCustom:
  399. WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
  400. if len(value) > 0 {
  401. handlers := strings.Join(value, "; ")
  402. WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers))
  403. } else {
  404. WriteStringAndCheck(buf, " flags_completion+=(:)\n")
  405. }
  406. case BashCompSubdirsInDir:
  407. WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
  408. var ext string
  409. if len(value) == 1 {
  410. ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Root().Name()) + value[0]
  411. } else {
  412. ext = "_filedir -d"
  413. }
  414. WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
  415. }
  416. }
  417. }
  418. const cbn = "\")\n"
  419. func writeShortFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) {
  420. name := flag.Shorthand
  421. format := " "
  422. if len(flag.NoOptDefVal) == 0 {
  423. format += "two_word_"
  424. }
  425. format += "flags+=(\"-%s" + cbn
  426. WriteStringAndCheck(buf, fmt.Sprintf(format, name))
  427. writeFlagHandler(buf, "-"+name, flag.Annotations, cmd)
  428. }
  429. func writeFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) {
  430. name := flag.Name
  431. format := " flags+=(\"--%s"
  432. if len(flag.NoOptDefVal) == 0 {
  433. format += "="
  434. }
  435. format += cbn
  436. WriteStringAndCheck(buf, fmt.Sprintf(format, name))
  437. if len(flag.NoOptDefVal) == 0 {
  438. format = " two_word_flags+=(\"--%s" + cbn
  439. WriteStringAndCheck(buf, fmt.Sprintf(format, name))
  440. }
  441. writeFlagHandler(buf, "--"+name, flag.Annotations, cmd)
  442. }
  443. func writeLocalNonPersistentFlag(buf io.StringWriter, flag *pflag.Flag) {
  444. name := flag.Name
  445. format := " local_nonpersistent_flags+=(\"--%[1]s" + cbn
  446. if len(flag.NoOptDefVal) == 0 {
  447. format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn
  448. }
  449. WriteStringAndCheck(buf, fmt.Sprintf(format, name))
  450. if len(flag.Shorthand) > 0 {
  451. WriteStringAndCheck(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%s\")\n", flag.Shorthand))
  452. }
  453. }
  454. // Setup annotations for go completions for registered flags
  455. func prepareCustomAnnotationsForFlags(cmd *Command) {
  456. flagCompletionMutex.RLock()
  457. defer flagCompletionMutex.RUnlock()
  458. for flag := range flagCompletionFunctions {
  459. // Make sure the completion script calls the __*_go_custom_completion function for
  460. // every registered flag. We need to do this here (and not when the flag was registered
  461. // for completion) so that we can know the root command name for the prefix
  462. // of __<prefix>_go_custom_completion
  463. if flag.Annotations == nil {
  464. flag.Annotations = map[string][]string{}
  465. }
  466. flag.Annotations[BashCompCustom] = []string{fmt.Sprintf("__%[1]s_handle_go_custom_completion", cmd.Root().Name())}
  467. }
  468. }
  469. func writeFlags(buf io.StringWriter, cmd *Command) {
  470. prepareCustomAnnotationsForFlags(cmd)
  471. WriteStringAndCheck(buf, ` flags=()
  472. two_word_flags=()
  473. local_nonpersistent_flags=()
  474. flags_with_completion=()
  475. flags_completion=()
  476. `)
  477. localNonPersistentFlags := cmd.LocalNonPersistentFlags()
  478. cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
  479. if nonCompletableFlag(flag) {
  480. return
  481. }
  482. writeFlag(buf, flag, cmd)
  483. if len(flag.Shorthand) > 0 {
  484. writeShortFlag(buf, flag, cmd)
  485. }
  486. // localNonPersistentFlags are used to stop the completion of subcommands when one is set
  487. // if TraverseChildren is true we should allow to complete subcommands
  488. if localNonPersistentFlags.Lookup(flag.Name) != nil && !cmd.Root().TraverseChildren {
  489. writeLocalNonPersistentFlag(buf, flag)
  490. }
  491. })
  492. cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
  493. if nonCompletableFlag(flag) {
  494. return
  495. }
  496. writeFlag(buf, flag, cmd)
  497. if len(flag.Shorthand) > 0 {
  498. writeShortFlag(buf, flag, cmd)
  499. }
  500. })
  501. WriteStringAndCheck(buf, "\n")
  502. }
  503. func writeRequiredFlag(buf io.StringWriter, cmd *Command) {
  504. WriteStringAndCheck(buf, " must_have_one_flag=()\n")
  505. flags := cmd.NonInheritedFlags()
  506. flags.VisitAll(func(flag *pflag.Flag) {
  507. if nonCompletableFlag(flag) {
  508. return
  509. }
  510. for key := range flag.Annotations {
  511. switch key {
  512. case BashCompOneRequiredFlag:
  513. format := " must_have_one_flag+=(\"--%s"
  514. if flag.Value.Type() != "bool" {
  515. format += "="
  516. }
  517. format += cbn
  518. WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name))
  519. if len(flag.Shorthand) > 0 {
  520. WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand))
  521. }
  522. }
  523. }
  524. })
  525. }
  526. func writeRequiredNouns(buf io.StringWriter, cmd *Command) {
  527. WriteStringAndCheck(buf, " must_have_one_noun=()\n")
  528. sort.Strings(cmd.ValidArgs)
  529. for _, value := range cmd.ValidArgs {
  530. // Remove any description that may be included following a tab character.
  531. // Descriptions are not supported by bash completion.
  532. value = strings.Split(value, "\t")[0]
  533. WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_noun+=(%q)\n", value))
  534. }
  535. if cmd.ValidArgsFunction != nil {
  536. WriteStringAndCheck(buf, " has_completion_function=1\n")
  537. }
  538. }
  539. func writeCmdAliases(buf io.StringWriter, cmd *Command) {
  540. if len(cmd.Aliases) == 0 {
  541. return
  542. }
  543. sort.Strings(cmd.Aliases)
  544. WriteStringAndCheck(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then`, "\n"))
  545. for _, value := range cmd.Aliases {
  546. WriteStringAndCheck(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value))
  547. WriteStringAndCheck(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name()))
  548. }
  549. WriteStringAndCheck(buf, ` fi`)
  550. WriteStringAndCheck(buf, "\n")
  551. }
  552. func writeArgAliases(buf io.StringWriter, cmd *Command) {
  553. WriteStringAndCheck(buf, " noun_aliases=()\n")
  554. sort.Strings(cmd.ArgAliases)
  555. for _, value := range cmd.ArgAliases {
  556. WriteStringAndCheck(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value))
  557. }
  558. }
  559. func gen(buf io.StringWriter, cmd *Command) {
  560. for _, c := range cmd.Commands() {
  561. if !c.IsAvailableCommand() && c != cmd.helpCommand {
  562. continue
  563. }
  564. gen(buf, c)
  565. }
  566. commandName := cmd.CommandPath()
  567. commandName = strings.Replace(commandName, " ", "_", -1)
  568. commandName = strings.Replace(commandName, ":", "__", -1)
  569. if cmd.Root() == cmd {
  570. WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName))
  571. } else {
  572. WriteStringAndCheck(buf, fmt.Sprintf("_%s()\n{\n", commandName))
  573. }
  574. WriteStringAndCheck(buf, fmt.Sprintf(" last_command=%q\n", commandName))
  575. WriteStringAndCheck(buf, "\n")
  576. WriteStringAndCheck(buf, " command_aliases=()\n")
  577. WriteStringAndCheck(buf, "\n")
  578. writeCommands(buf, cmd)
  579. writeFlags(buf, cmd)
  580. writeRequiredFlag(buf, cmd)
  581. writeRequiredNouns(buf, cmd)
  582. writeArgAliases(buf, cmd)
  583. WriteStringAndCheck(buf, "}\n\n")
  584. }
  585. // GenBashCompletion generates bash completion file and writes to the passed writer.
  586. func (c *Command) GenBashCompletion(w io.Writer) error {
  587. buf := new(bytes.Buffer)
  588. writePreamble(buf, c.Name())
  589. if len(c.BashCompletionFunction) > 0 {
  590. buf.WriteString(c.BashCompletionFunction + "\n")
  591. }
  592. gen(buf, c)
  593. writePostscript(buf, c.Name())
  594. _, err := buf.WriteTo(w)
  595. return err
  596. }
  597. func nonCompletableFlag(flag *pflag.Flag) bool {
  598. return flag.Hidden || len(flag.Deprecated) > 0
  599. }
  600. // GenBashCompletionFile generates bash completion file.
  601. func (c *Command) GenBashCompletionFile(filename string) error {
  602. outFile, err := os.Create(filename)
  603. if err != nil {
  604. return err
  605. }
  606. defer outFile.Close()
  607. return c.GenBashCompletion(outFile)
  608. }