bash_completions.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. package cobra
  2. import (
  3. "fmt"
  4. "io"
  5. "os"
  6. "sort"
  7. "strings"
  8. "github.com/spf13/pflag"
  9. )
  10. const (
  11. BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extentions"
  12. BashCompCustom = "cobra_annotation_bash_completion_custom"
  13. BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag"
  14. BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir"
  15. )
  16. func preamble(out io.Writer, name string) error {
  17. _, err := fmt.Fprintf(out, "# bash completion for %-36s -*- shell-script -*-\n", name)
  18. if err != nil {
  19. return err
  20. }
  21. _, err = fmt.Fprint(out, `
  22. __debug()
  23. {
  24. if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then
  25. echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
  26. fi
  27. }
  28. # Homebrew on Macs have version 1.3 of bash-completion which doesn't include
  29. # _init_completion. This is a very minimal version of that function.
  30. __my_init_completion()
  31. {
  32. COMPREPLY=()
  33. _get_comp_words_by_ref "$@" cur prev words cword
  34. }
  35. __index_of_word()
  36. {
  37. local w word=$1
  38. shift
  39. index=0
  40. for w in "$@"; do
  41. [[ $w = "$word" ]] && return
  42. index=$((index+1))
  43. done
  44. index=-1
  45. }
  46. __contains_word()
  47. {
  48. local w word=$1; shift
  49. for w in "$@"; do
  50. [[ $w = "$word" ]] && return
  51. done
  52. return 1
  53. }
  54. __handle_reply()
  55. {
  56. __debug "${FUNCNAME[0]}"
  57. case $cur in
  58. -*)
  59. if [[ $(type -t compopt) = "builtin" ]]; then
  60. compopt -o nospace
  61. fi
  62. local allflags
  63. if [ ${#must_have_one_flag[@]} -ne 0 ]; then
  64. allflags=("${must_have_one_flag[@]}")
  65. else
  66. allflags=("${flags[*]} ${two_word_flags[*]}")
  67. fi
  68. COMPREPLY=( $(compgen -W "${allflags[*]}" -- "$cur") )
  69. if [[ $(type -t compopt) = "builtin" ]]; then
  70. [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
  71. fi
  72. # complete after --flag=abc
  73. if [[ $cur == *=* ]]; then
  74. if [[ $(type -t compopt) = "builtin" ]]; then
  75. compopt +o nospace
  76. fi
  77. local index flag
  78. flag="${cur%%=*}"
  79. __index_of_word "${flag}" "${flags_with_completion[@]}"
  80. if [[ ${index} -ge 0 ]]; then
  81. COMPREPLY=()
  82. PREFIX=""
  83. cur="${cur#*=}"
  84. ${flags_completion[${index}]}
  85. if [ -n "${ZSH_VERSION}" ]; then
  86. # zfs completion needs --flag= prefix
  87. eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
  88. fi
  89. fi
  90. fi
  91. return 0;
  92. ;;
  93. esac
  94. # check if we are handling a flag with special work handling
  95. local index
  96. __index_of_word "${prev}" "${flags_with_completion[@]}"
  97. if [[ ${index} -ge 0 ]]; then
  98. ${flags_completion[${index}]}
  99. return
  100. fi
  101. # we are parsing a flag and don't have a special handler, no completion
  102. if [[ ${cur} != "${words[cword]}" ]]; then
  103. return
  104. fi
  105. local completions
  106. completions=("${commands[@]}")
  107. if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
  108. completions=("${must_have_one_noun[@]}")
  109. fi
  110. if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
  111. completions+=("${must_have_one_flag[@]}")
  112. fi
  113. COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") )
  114. if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
  115. COMPREPLY=( $(compgen -W "${noun_aliases[*]}" -- "$cur") )
  116. fi
  117. if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
  118. declare -F __custom_func >/dev/null && __custom_func
  119. fi
  120. __ltrim_colon_completions "$cur"
  121. }
  122. # The arguments should be in the form "ext1|ext2|extn"
  123. __handle_filename_extension_flag()
  124. {
  125. local ext="$1"
  126. _filedir "@(${ext})"
  127. }
  128. __handle_subdirs_in_dir_flag()
  129. {
  130. local dir="$1"
  131. pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1
  132. }
  133. __handle_flag()
  134. {
  135. __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  136. # if a command required a flag, and we found it, unset must_have_one_flag()
  137. local flagname=${words[c]}
  138. local flagvalue
  139. # if the word contained an =
  140. if [[ ${words[c]} == *"="* ]]; then
  141. flagvalue=${flagname#*=} # take in as flagvalue after the =
  142. flagname=${flagname%%=*} # strip everything after the =
  143. flagname="${flagname}=" # but put the = back
  144. fi
  145. __debug "${FUNCNAME[0]}: looking for ${flagname}"
  146. if __contains_word "${flagname}" "${must_have_one_flag[@]}"; then
  147. must_have_one_flag=()
  148. fi
  149. # if you set a flag which only applies to this command, don't show subcommands
  150. if __contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
  151. commands=()
  152. fi
  153. # keep flag value with flagname as flaghash
  154. if [ -n "${flagvalue}" ] ; then
  155. flaghash[${flagname}]=${flagvalue}
  156. elif [ -n "${words[ $((c+1)) ]}" ] ; then
  157. flaghash[${flagname}]=${words[ $((c+1)) ]}
  158. else
  159. flaghash[${flagname}]="true" # pad "true" for bool flag
  160. fi
  161. # skip the argument to a two word flag
  162. if __contains_word "${words[c]}" "${two_word_flags[@]}"; then
  163. c=$((c+1))
  164. # if we are looking for a flags value, don't show commands
  165. if [[ $c -eq $cword ]]; then
  166. commands=()
  167. fi
  168. fi
  169. c=$((c+1))
  170. }
  171. __handle_noun()
  172. {
  173. __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  174. if __contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
  175. must_have_one_noun=()
  176. elif __contains_word "${words[c]}" "${noun_aliases[@]}"; then
  177. must_have_one_noun=()
  178. fi
  179. nouns+=("${words[c]}")
  180. c=$((c+1))
  181. }
  182. __handle_command()
  183. {
  184. __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  185. local next_command
  186. if [[ -n ${last_command} ]]; then
  187. next_command="_${last_command}_${words[c]//:/__}"
  188. else
  189. if [[ $c -eq 0 ]]; then
  190. next_command="_$(basename "${words[c]//:/__}")"
  191. else
  192. next_command="_${words[c]//:/__}"
  193. fi
  194. fi
  195. c=$((c+1))
  196. __debug "${FUNCNAME[0]}: looking for ${next_command}"
  197. declare -F $next_command >/dev/null && $next_command
  198. }
  199. __handle_word()
  200. {
  201. if [[ $c -ge $cword ]]; then
  202. __handle_reply
  203. return
  204. fi
  205. __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
  206. if [[ "${words[c]}" == -* ]]; then
  207. __handle_flag
  208. elif __contains_word "${words[c]}" "${commands[@]}"; then
  209. __handle_command
  210. elif [[ $c -eq 0 ]] && __contains_word "$(basename "${words[c]}")" "${commands[@]}"; then
  211. __handle_command
  212. else
  213. __handle_noun
  214. fi
  215. __handle_word
  216. }
  217. `)
  218. return err
  219. }
  220. func postscript(w io.Writer, name string) error {
  221. name = strings.Replace(name, ":", "__", -1)
  222. _, err := fmt.Fprintf(w, "__start_%s()\n", name)
  223. if err != nil {
  224. return err
  225. }
  226. _, err = fmt.Fprintf(w, `{
  227. local cur prev words cword
  228. declare -A flaghash 2>/dev/null || :
  229. if declare -F _init_completion >/dev/null 2>&1; then
  230. _init_completion -s || return
  231. else
  232. __my_init_completion -n "=" || return
  233. fi
  234. local c=0
  235. local flags=()
  236. local two_word_flags=()
  237. local local_nonpersistent_flags=()
  238. local flags_with_completion=()
  239. local flags_completion=()
  240. local commands=("%s")
  241. local must_have_one_flag=()
  242. local must_have_one_noun=()
  243. local last_command
  244. local nouns=()
  245. __handle_word
  246. }
  247. `, name)
  248. if err != nil {
  249. return err
  250. }
  251. _, err = fmt.Fprintf(w, `if [[ $(type -t compopt) = "builtin" ]]; then
  252. complete -o default -F __start_%s %s
  253. else
  254. complete -o default -o nospace -F __start_%s %s
  255. fi
  256. `, name, name, name, name)
  257. if err != nil {
  258. return err
  259. }
  260. _, err = fmt.Fprintf(w, "# ex: ts=4 sw=4 et filetype=sh\n")
  261. return err
  262. }
  263. func writeCommands(cmd *Command, w io.Writer) error {
  264. if _, err := fmt.Fprintf(w, " commands=()\n"); err != nil {
  265. return err
  266. }
  267. for _, c := range cmd.Commands() {
  268. if !c.IsAvailableCommand() || c == cmd.helpCommand {
  269. continue
  270. }
  271. if _, err := fmt.Fprintf(w, " commands+=(%q)\n", c.Name()); err != nil {
  272. return err
  273. }
  274. }
  275. _, err := fmt.Fprintf(w, "\n")
  276. return err
  277. }
  278. func writeFlagHandler(name string, annotations map[string][]string, w io.Writer) error {
  279. for key, value := range annotations {
  280. switch key {
  281. case BashCompFilenameExt:
  282. _, err := fmt.Fprintf(w, " flags_with_completion+=(%q)\n", name)
  283. if err != nil {
  284. return err
  285. }
  286. if len(value) > 0 {
  287. ext := "__handle_filename_extension_flag " + strings.Join(value, "|")
  288. _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext)
  289. } else {
  290. ext := "_filedir"
  291. _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext)
  292. }
  293. if err != nil {
  294. return err
  295. }
  296. case BashCompCustom:
  297. _, err := fmt.Fprintf(w, " flags_with_completion+=(%q)\n", name)
  298. if err != nil {
  299. return err
  300. }
  301. if len(value) > 0 {
  302. handlers := strings.Join(value, "; ")
  303. _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", handlers)
  304. } else {
  305. _, err = fmt.Fprintf(w, " flags_completion+=(:)\n")
  306. }
  307. if err != nil {
  308. return err
  309. }
  310. case BashCompSubdirsInDir:
  311. _, err := fmt.Fprintf(w, " flags_with_completion+=(%q)\n", name)
  312. if len(value) == 1 {
  313. ext := "__handle_subdirs_in_dir_flag " + value[0]
  314. _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext)
  315. } else {
  316. ext := "_filedir -d"
  317. _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext)
  318. }
  319. if err != nil {
  320. return err
  321. }
  322. }
  323. }
  324. return nil
  325. }
  326. func writeShortFlag(flag *pflag.Flag, w io.Writer) error {
  327. b := (len(flag.NoOptDefVal) > 0)
  328. name := flag.Shorthand
  329. format := " "
  330. if !b {
  331. format += "two_word_"
  332. }
  333. format += "flags+=(\"-%s\")\n"
  334. if _, err := fmt.Fprintf(w, format, name); err != nil {
  335. return err
  336. }
  337. return writeFlagHandler("-"+name, flag.Annotations, w)
  338. }
  339. func writeFlag(flag *pflag.Flag, w io.Writer) error {
  340. b := (len(flag.NoOptDefVal) > 0)
  341. name := flag.Name
  342. format := " flags+=(\"--%s"
  343. if !b {
  344. format += "="
  345. }
  346. format += "\")\n"
  347. if _, err := fmt.Fprintf(w, format, name); err != nil {
  348. return err
  349. }
  350. return writeFlagHandler("--"+name, flag.Annotations, w)
  351. }
  352. func writeLocalNonPersistentFlag(flag *pflag.Flag, w io.Writer) error {
  353. b := (len(flag.NoOptDefVal) > 0)
  354. name := flag.Name
  355. format := " local_nonpersistent_flags+=(\"--%s"
  356. if !b {
  357. format += "="
  358. }
  359. format += "\")\n"
  360. if _, err := fmt.Fprintf(w, format, name); err != nil {
  361. return err
  362. }
  363. return nil
  364. }
  365. func writeFlags(cmd *Command, w io.Writer) error {
  366. _, err := fmt.Fprintf(w, ` flags=()
  367. two_word_flags=()
  368. local_nonpersistent_flags=()
  369. flags_with_completion=()
  370. flags_completion=()
  371. `)
  372. if err != nil {
  373. return err
  374. }
  375. localNonPersistentFlags := cmd.LocalNonPersistentFlags()
  376. var visitErr error
  377. cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
  378. if err := writeFlag(flag, w); err != nil {
  379. visitErr = err
  380. return
  381. }
  382. if len(flag.Shorthand) > 0 {
  383. if err := writeShortFlag(flag, w); err != nil {
  384. visitErr = err
  385. return
  386. }
  387. }
  388. if localNonPersistentFlags.Lookup(flag.Name) != nil {
  389. if err := writeLocalNonPersistentFlag(flag, w); err != nil {
  390. visitErr = err
  391. return
  392. }
  393. }
  394. })
  395. if visitErr != nil {
  396. return visitErr
  397. }
  398. cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
  399. if err := writeFlag(flag, w); err != nil {
  400. visitErr = err
  401. return
  402. }
  403. if len(flag.Shorthand) > 0 {
  404. if err := writeShortFlag(flag, w); err != nil {
  405. visitErr = err
  406. return
  407. }
  408. }
  409. })
  410. if visitErr != nil {
  411. return visitErr
  412. }
  413. _, err = fmt.Fprintf(w, "\n")
  414. return err
  415. }
  416. func writeRequiredFlag(cmd *Command, w io.Writer) error {
  417. if _, err := fmt.Fprintf(w, " must_have_one_flag=()\n"); err != nil {
  418. return err
  419. }
  420. flags := cmd.NonInheritedFlags()
  421. var visitErr error
  422. flags.VisitAll(func(flag *pflag.Flag) {
  423. for key := range flag.Annotations {
  424. switch key {
  425. case BashCompOneRequiredFlag:
  426. format := " must_have_one_flag+=(\"--%s"
  427. b := (flag.Value.Type() == "bool")
  428. if !b {
  429. format += "="
  430. }
  431. format += "\")\n"
  432. if _, err := fmt.Fprintf(w, format, flag.Name); err != nil {
  433. visitErr = err
  434. return
  435. }
  436. if len(flag.Shorthand) > 0 {
  437. if _, err := fmt.Fprintf(w, " must_have_one_flag+=(\"-%s\")\n", flag.Shorthand); err != nil {
  438. visitErr = err
  439. return
  440. }
  441. }
  442. }
  443. }
  444. })
  445. return visitErr
  446. }
  447. func writeRequiredNouns(cmd *Command, w io.Writer) error {
  448. if _, err := fmt.Fprintf(w, " must_have_one_noun=()\n"); err != nil {
  449. return err
  450. }
  451. sort.Sort(sort.StringSlice(cmd.ValidArgs))
  452. for _, value := range cmd.ValidArgs {
  453. if _, err := fmt.Fprintf(w, " must_have_one_noun+=(%q)\n", value); err != nil {
  454. return err
  455. }
  456. }
  457. return nil
  458. }
  459. func writeArgAliases(cmd *Command, w io.Writer) error {
  460. if _, err := fmt.Fprintf(w, " noun_aliases=()\n"); err != nil {
  461. return err
  462. }
  463. sort.Sort(sort.StringSlice(cmd.ArgAliases))
  464. for _, value := range cmd.ArgAliases {
  465. if _, err := fmt.Fprintf(w, " noun_aliases+=(%q)\n", value); err != nil {
  466. return err
  467. }
  468. }
  469. return nil
  470. }
  471. func gen(cmd *Command, w io.Writer) error {
  472. for _, c := range cmd.Commands() {
  473. if !c.IsAvailableCommand() || c == cmd.helpCommand {
  474. continue
  475. }
  476. if err := gen(c, w); err != nil {
  477. return err
  478. }
  479. }
  480. commandName := cmd.CommandPath()
  481. commandName = strings.Replace(commandName, " ", "_", -1)
  482. commandName = strings.Replace(commandName, ":", "__", -1)
  483. if _, err := fmt.Fprintf(w, "_%s()\n{\n", commandName); err != nil {
  484. return err
  485. }
  486. if _, err := fmt.Fprintf(w, " last_command=%q\n", commandName); err != nil {
  487. return err
  488. }
  489. if err := writeCommands(cmd, w); err != nil {
  490. return err
  491. }
  492. if err := writeFlags(cmd, w); err != nil {
  493. return err
  494. }
  495. if err := writeRequiredFlag(cmd, w); err != nil {
  496. return err
  497. }
  498. if err := writeRequiredNouns(cmd, w); err != nil {
  499. return err
  500. }
  501. if err := writeArgAliases(cmd, w); err != nil {
  502. return err
  503. }
  504. if _, err := fmt.Fprintf(w, "}\n\n"); err != nil {
  505. return err
  506. }
  507. return nil
  508. }
  509. func (cmd *Command) GenBashCompletion(w io.Writer) error {
  510. if err := preamble(w, cmd.Name()); err != nil {
  511. return err
  512. }
  513. if len(cmd.BashCompletionFunction) > 0 {
  514. if _, err := fmt.Fprintf(w, "%s\n", cmd.BashCompletionFunction); err != nil {
  515. return err
  516. }
  517. }
  518. if err := gen(cmd, w); err != nil {
  519. return err
  520. }
  521. return postscript(w, cmd.Name())
  522. }
  523. func (cmd *Command) GenBashCompletionFile(filename string) error {
  524. outFile, err := os.Create(filename)
  525. if err != nil {
  526. return err
  527. }
  528. defer outFile.Close()
  529. return cmd.GenBashCompletion(outFile)
  530. }
  531. // MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag, if it exists.
  532. func (cmd *Command) MarkFlagRequired(name string) error {
  533. return MarkFlagRequired(cmd.Flags(), name)
  534. }
  535. // MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag, if it exists.
  536. func (cmd *Command) MarkPersistentFlagRequired(name string) error {
  537. return MarkFlagRequired(cmd.PersistentFlags(), name)
  538. }
  539. // MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag in the flag set, if it exists.
  540. func MarkFlagRequired(flags *pflag.FlagSet, name string) error {
  541. return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"})
  542. }
  543. // MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists.
  544. // Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
  545. func (cmd *Command) MarkFlagFilename(name string, extensions ...string) error {
  546. return MarkFlagFilename(cmd.Flags(), name, extensions...)
  547. }
  548. // MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists.
  549. // Generated bash autocompletion will call the bash function f for the flag.
  550. func (cmd *Command) MarkFlagCustom(name string, f string) error {
  551. return MarkFlagCustom(cmd.Flags(), name, f)
  552. }
  553. // MarkPersistentFlagFilename adds the BashCompFilenameExt annotation to the named persistent flag, if it exists.
  554. // Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
  555. func (cmd *Command) MarkPersistentFlagFilename(name string, extensions ...string) error {
  556. return MarkFlagFilename(cmd.PersistentFlags(), name, extensions...)
  557. }
  558. // MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag in the flag set, if it exists.
  559. // Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
  560. func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error {
  561. return flags.SetAnnotation(name, BashCompFilenameExt, extensions)
  562. }
  563. // MarkFlagCustom adds the BashCompCustom annotation to the named flag in the flag set, if it exists.
  564. // Generated bash autocompletion will call the bash function f for the flag.
  565. func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error {
  566. return flags.SetAnnotation(name, BashCompCustom, []string{f})
  567. }