custom_completions.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. package cobra
  2. import (
  3. "fmt"
  4. "os"
  5. "strings"
  6. "github.com/spf13/pflag"
  7. )
  8. const (
  9. // ShellCompRequestCmd is the name of the hidden command that is used to request
  10. // completion results from the program. It is used by the shell completion scripts.
  11. ShellCompRequestCmd = "__complete"
  12. // ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
  13. // completion results without their description. It is used by the shell completion scripts.
  14. ShellCompNoDescRequestCmd = "__completeNoDesc"
  15. )
  16. // Global map of flag completion functions.
  17. var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
  18. // ShellCompDirective is a bit map representing the different behaviors the shell
  19. // can be instructed to have once completions have been provided.
  20. type ShellCompDirective int
  21. const (
  22. // ShellCompDirectiveError indicates an error occurred and completions should be ignored.
  23. ShellCompDirectiveError ShellCompDirective = 1 << iota
  24. // ShellCompDirectiveNoSpace indicates that the shell should not add a space
  25. // after the completion even if there is a single completion provided.
  26. ShellCompDirectiveNoSpace
  27. // ShellCompDirectiveNoFileComp indicates that the shell should not provide
  28. // file completion even when no completion is provided.
  29. // This currently does not work for zsh or bash < 4
  30. ShellCompDirectiveNoFileComp
  31. // ShellCompDirectiveFilterFileExt indicates that the provided completions
  32. // should be used as file extension filters.
  33. // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
  34. // is a shortcut to using this directive explicitly. The BashCompFilenameExt
  35. // annotation can also be used to obtain the same behavior for flags.
  36. ShellCompDirectiveFilterFileExt
  37. // ShellCompDirectiveFilterDirs indicates that only directory names should
  38. // be provided in file completion. To request directory names within another
  39. // directory, the returned completions should specify the directory within
  40. // which to search. The BashCompSubdirsInDir annotation can be used to
  41. // obtain the same behavior but only for flags.
  42. ShellCompDirectiveFilterDirs
  43. // ===========================================================================
  44. // All directives using iota should be above this one.
  45. // For internal use.
  46. shellCompDirectiveMaxValue
  47. // ShellCompDirectiveDefault indicates to let the shell perform its default
  48. // behavior after completions have been provided.
  49. // This one must be last to avoid messing up the iota count.
  50. ShellCompDirectiveDefault ShellCompDirective = 0
  51. )
  52. // RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag.
  53. func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)) error {
  54. flag := c.Flag(flagName)
  55. if flag == nil {
  56. return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
  57. }
  58. if _, exists := flagCompletionFunctions[flag]; exists {
  59. return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
  60. }
  61. flagCompletionFunctions[flag] = f
  62. return nil
  63. }
  64. // Returns a string listing the different directive enabled in the specified parameter
  65. func (d ShellCompDirective) string() string {
  66. var directives []string
  67. if d&ShellCompDirectiveError != 0 {
  68. directives = append(directives, "ShellCompDirectiveError")
  69. }
  70. if d&ShellCompDirectiveNoSpace != 0 {
  71. directives = append(directives, "ShellCompDirectiveNoSpace")
  72. }
  73. if d&ShellCompDirectiveNoFileComp != 0 {
  74. directives = append(directives, "ShellCompDirectiveNoFileComp")
  75. }
  76. if d&ShellCompDirectiveFilterFileExt != 0 {
  77. directives = append(directives, "ShellCompDirectiveFilterFileExt")
  78. }
  79. if d&ShellCompDirectiveFilterDirs != 0 {
  80. directives = append(directives, "ShellCompDirectiveFilterDirs")
  81. }
  82. if len(directives) == 0 {
  83. directives = append(directives, "ShellCompDirectiveDefault")
  84. }
  85. if d >= shellCompDirectiveMaxValue {
  86. return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
  87. }
  88. return strings.Join(directives, ", ")
  89. }
  90. // Adds a special hidden command that can be used to request custom completions.
  91. func (c *Command) initCompleteCmd(args []string) {
  92. completeCmd := &Command{
  93. Use: fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
  94. Aliases: []string{ShellCompNoDescRequestCmd},
  95. DisableFlagsInUseLine: true,
  96. Hidden: true,
  97. DisableFlagParsing: true,
  98. Args: MinimumNArgs(1),
  99. Short: "Request shell completion choices for the specified command-line",
  100. Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s",
  101. "to request completion choices for the specified command-line.", ShellCompRequestCmd),
  102. Run: func(cmd *Command, args []string) {
  103. finalCmd, completions, directive, err := cmd.getCompletions(args)
  104. if err != nil {
  105. CompErrorln(err.Error())
  106. // Keep going for multiple reasons:
  107. // 1- There could be some valid completions even though there was an error
  108. // 2- Even without completions, we need to print the directive
  109. }
  110. noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
  111. for _, comp := range completions {
  112. if noDescriptions {
  113. // Remove any description that may be included following a tab character.
  114. comp = strings.Split(comp, "\t")[0]
  115. }
  116. // Make sure we only write the first line to the output.
  117. // This is needed if a description contains a linebreak.
  118. // Otherwise the shell scripts will interpret the other lines as new flags
  119. // and could therefore provide a wrong completion.
  120. comp = strings.Split(comp, "\n")[0]
  121. // Finally trim the completion. This is especially important to get rid
  122. // of a trailing tab when there are no description following it.
  123. // For example, a sub-command without a description should not be completed
  124. // with a tab at the end (or else zsh will show a -- following it
  125. // although there is no description).
  126. comp = strings.TrimSpace(comp)
  127. // Print each possible completion to stdout for the completion script to consume.
  128. fmt.Fprintln(finalCmd.OutOrStdout(), comp)
  129. }
  130. if directive >= shellCompDirectiveMaxValue {
  131. directive = ShellCompDirectiveDefault
  132. }
  133. // As the last printout, print the completion directive for the completion script to parse.
  134. // The directive integer must be that last character following a single colon (:).
  135. // The completion script expects :<directive>
  136. fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
  137. // Print some helpful info to stderr for the user to understand.
  138. // Output from stderr must be ignored by the completion script.
  139. fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string())
  140. },
  141. }
  142. c.AddCommand(completeCmd)
  143. subCmd, _, err := c.Find(args)
  144. if err != nil || subCmd.Name() != ShellCompRequestCmd {
  145. // Only create this special command if it is actually being called.
  146. // This reduces possible side-effects of creating such a command;
  147. // for example, having this command would cause problems to a
  148. // cobra program that only consists of the root command, since this
  149. // command would cause the root command to suddenly have a subcommand.
  150. c.RemoveCommand(completeCmd)
  151. }
  152. }
  153. func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
  154. // The last argument, which is not completely typed by the user,
  155. // should not be part of the list of arguments
  156. toComplete := args[len(args)-1]
  157. trimmedArgs := args[:len(args)-1]
  158. var finalCmd *Command
  159. var finalArgs []string
  160. var err error
  161. // Find the real command for which completion must be performed
  162. // check if we need to traverse here to parse local flags on parent commands
  163. if c.Root().TraverseChildren {
  164. finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs)
  165. } else {
  166. finalCmd, finalArgs, err = c.Root().Find(trimmedArgs)
  167. }
  168. if err != nil {
  169. // Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
  170. return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
  171. }
  172. // Check if we are doing flag value completion before parsing the flags.
  173. // This is important because if we are completing a flag value, we need to also
  174. // remove the flag name argument from the list of finalArgs or else the parsing
  175. // could fail due to an invalid value (incomplete) for the flag.
  176. flag, finalArgs, toComplete, err := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
  177. if err != nil {
  178. // Error while attempting to parse flags
  179. return finalCmd, []string{}, ShellCompDirectiveDefault, err
  180. }
  181. // Parse the flags early so we can check if required flags are set
  182. if err = finalCmd.ParseFlags(finalArgs); err != nil {
  183. return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
  184. }
  185. if flag != nil {
  186. // Check if we are completing a flag value subject to annotations
  187. if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
  188. if len(validExts) != 0 {
  189. // File completion filtered by extensions
  190. return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil
  191. }
  192. // The annotation requests simple file completion. There is no reason to do
  193. // that since it is the default behavior anyway. Let's ignore this annotation
  194. // in case the program also registered a completion function for this flag.
  195. // Even though it is a mistake on the program's side, let's be nice when we can.
  196. }
  197. if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present {
  198. if len(subDir) == 1 {
  199. // Directory completion from within a directory
  200. return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
  201. }
  202. // Directory completion
  203. return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
  204. }
  205. }
  206. // When doing completion of a flag name, as soon as an argument starts with
  207. // a '-' we know it is a flag. We cannot use isFlagArg() here as it requires
  208. // the flag name to be complete
  209. if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") {
  210. var completions []string
  211. // First check for required flags
  212. completions = completeRequireFlags(finalCmd, toComplete)
  213. // If we have not found any required flags, only then can we show regular flags
  214. if len(completions) == 0 {
  215. doCompleteFlags := func(flag *pflag.Flag) {
  216. if !flag.Changed ||
  217. strings.Contains(flag.Value.Type(), "Slice") ||
  218. strings.Contains(flag.Value.Type(), "Array") {
  219. // If the flag is not already present, or if it can be specified multiple times (Array or Slice)
  220. // we suggest it as a completion
  221. completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
  222. }
  223. }
  224. // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
  225. // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
  226. // non-inherited flags.
  227. finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
  228. doCompleteFlags(flag)
  229. })
  230. finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
  231. doCompleteFlags(flag)
  232. })
  233. }
  234. directive := ShellCompDirectiveNoFileComp
  235. if len(completions) == 1 && strings.HasSuffix(completions[0], "=") {
  236. // If there is a single completion, the shell usually adds a space
  237. // after the completion. We don't want that if the flag ends with an =
  238. directive = ShellCompDirectiveNoSpace
  239. }
  240. return finalCmd, completions, directive, nil
  241. }
  242. // We only remove the flags from the arguments if DisableFlagParsing is not set.
  243. // This is important for commands which have requested to do their own flag completion.
  244. if !finalCmd.DisableFlagParsing {
  245. finalArgs = finalCmd.Flags().Args()
  246. }
  247. var completions []string
  248. directive := ShellCompDirectiveDefault
  249. if flag == nil {
  250. foundLocalNonPersistentFlag := false
  251. // If TraverseChildren is true on the root command we don't check for
  252. // local flags because we can use a local flag on a parent command
  253. if !finalCmd.Root().TraverseChildren {
  254. // Check if there are any local, non-persistent flags on the command-line
  255. localNonPersistentFlags := finalCmd.LocalNonPersistentFlags()
  256. finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
  257. if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed {
  258. foundLocalNonPersistentFlag = true
  259. }
  260. })
  261. }
  262. // Complete subcommand names, including the help command
  263. if len(finalArgs) == 0 && !foundLocalNonPersistentFlag {
  264. // We only complete sub-commands if:
  265. // - there are no arguments on the command-line and
  266. // - there are no local, non-peristent flag on the command-line or TraverseChildren is true
  267. for _, subCmd := range finalCmd.Commands() {
  268. if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
  269. if strings.HasPrefix(subCmd.Name(), toComplete) {
  270. completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
  271. }
  272. directive = ShellCompDirectiveNoFileComp
  273. }
  274. }
  275. }
  276. // Complete required flags even without the '-' prefix
  277. completions = append(completions, completeRequireFlags(finalCmd, toComplete)...)
  278. // Always complete ValidArgs, even if we are completing a subcommand name.
  279. // This is for commands that have both subcommands and ValidArgs.
  280. if len(finalCmd.ValidArgs) > 0 {
  281. if len(finalArgs) == 0 {
  282. // ValidArgs are only for the first argument
  283. for _, validArg := range finalCmd.ValidArgs {
  284. if strings.HasPrefix(validArg, toComplete) {
  285. completions = append(completions, validArg)
  286. }
  287. }
  288. directive = ShellCompDirectiveNoFileComp
  289. // If no completions were found within commands or ValidArgs,
  290. // see if there are any ArgAliases that should be completed.
  291. if len(completions) == 0 {
  292. for _, argAlias := range finalCmd.ArgAliases {
  293. if strings.HasPrefix(argAlias, toComplete) {
  294. completions = append(completions, argAlias)
  295. }
  296. }
  297. }
  298. }
  299. // If there are ValidArgs specified (even if they don't match), we stop completion.
  300. // Only one of ValidArgs or ValidArgsFunction can be used for a single command.
  301. return finalCmd, completions, directive, nil
  302. }
  303. // Let the logic continue so as to add any ValidArgsFunction completions,
  304. // even if we already found sub-commands.
  305. // This is for commands that have subcommands but also specify a ValidArgsFunction.
  306. }
  307. // Find the completion function for the flag or command
  308. var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
  309. if flag != nil {
  310. completionFn = flagCompletionFunctions[flag]
  311. } else {
  312. completionFn = finalCmd.ValidArgsFunction
  313. }
  314. if completionFn != nil {
  315. // Go custom completion defined for this flag or command.
  316. // Call the registered completion function to get the completions.
  317. var comps []string
  318. comps, directive = completionFn(finalCmd, finalArgs, toComplete)
  319. completions = append(completions, comps...)
  320. }
  321. return finalCmd, completions, directive, nil
  322. }
  323. func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
  324. if nonCompletableFlag(flag) {
  325. return []string{}
  326. }
  327. var completions []string
  328. flagName := "--" + flag.Name
  329. if strings.HasPrefix(flagName, toComplete) {
  330. // Flag without the =
  331. completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
  332. // Why suggest both long forms: --flag and --flag= ?
  333. // This forces the user to *always* have to type either an = or a space after the flag name.
  334. // Let's be nice and avoid making users have to do that.
  335. // Since boolean flags and shortname flags don't show the = form, let's go that route and never show it.
  336. // The = form will still work, we just won't suggest it.
  337. // This also makes the list of suggested flags shorter as we avoid all the = forms.
  338. //
  339. // if len(flag.NoOptDefVal) == 0 {
  340. // // Flag requires a value, so it can be suffixed with =
  341. // flagName += "="
  342. // completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
  343. // }
  344. }
  345. flagName = "-" + flag.Shorthand
  346. if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
  347. completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
  348. }
  349. return completions
  350. }
  351. func completeRequireFlags(finalCmd *Command, toComplete string) []string {
  352. var completions []string
  353. doCompleteRequiredFlags := func(flag *pflag.Flag) {
  354. if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {
  355. if !flag.Changed {
  356. // If the flag is not already present, we suggest it as a completion
  357. completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
  358. }
  359. }
  360. }
  361. // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
  362. // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
  363. // non-inherited flags.
  364. finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
  365. doCompleteRequiredFlags(flag)
  366. })
  367. finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
  368. doCompleteRequiredFlags(flag)
  369. })
  370. return completions
  371. }
  372. func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
  373. if finalCmd.DisableFlagParsing {
  374. // We only do flag completion if we are allowed to parse flags
  375. // This is important for commands which have requested to do their own flag completion.
  376. return nil, args, lastArg, nil
  377. }
  378. var flagName string
  379. trimmedArgs := args
  380. flagWithEqual := false
  381. // When doing completion of a flag name, as soon as an argument starts with
  382. // a '-' we know it is a flag. We cannot use isFlagArg() here as that function
  383. // requires the flag name to be complete
  384. if len(lastArg) > 0 && lastArg[0] == '-' {
  385. if index := strings.Index(lastArg, "="); index >= 0 {
  386. // Flag with an =
  387. flagName = strings.TrimLeft(lastArg[:index], "-")
  388. lastArg = lastArg[index+1:]
  389. flagWithEqual = true
  390. } else {
  391. // Normal flag completion
  392. return nil, args, lastArg, nil
  393. }
  394. }
  395. if len(flagName) == 0 {
  396. if len(args) > 0 {
  397. prevArg := args[len(args)-1]
  398. if isFlagArg(prevArg) {
  399. // Only consider the case where the flag does not contain an =.
  400. // If the flag contains an = it means it has already been fully processed,
  401. // so we don't need to deal with it here.
  402. if index := strings.Index(prevArg, "="); index < 0 {
  403. flagName = strings.TrimLeft(prevArg, "-")
  404. // Remove the uncompleted flag or else there could be an error created
  405. // for an invalid value for that flag
  406. trimmedArgs = args[:len(args)-1]
  407. }
  408. }
  409. }
  410. }
  411. if len(flagName) == 0 {
  412. // Not doing flag completion
  413. return nil, trimmedArgs, lastArg, nil
  414. }
  415. flag := findFlag(finalCmd, flagName)
  416. if flag == nil {
  417. // Flag not supported by this command, nothing to complete
  418. err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
  419. return nil, nil, "", err
  420. }
  421. if !flagWithEqual {
  422. if len(flag.NoOptDefVal) != 0 {
  423. // We had assumed dealing with a two-word flag but the flag is a boolean flag.
  424. // In that case, there is no value following it, so we are not really doing flag completion.
  425. // Reset everything to do noun completion.
  426. trimmedArgs = args
  427. flag = nil
  428. }
  429. }
  430. return flag, trimmedArgs, lastArg, nil
  431. }
  432. func findFlag(cmd *Command, name string) *pflag.Flag {
  433. flagSet := cmd.Flags()
  434. if len(name) == 1 {
  435. // First convert the short flag into a long flag
  436. // as the cmd.Flag() search only accepts long flags
  437. if short := flagSet.ShorthandLookup(name); short != nil {
  438. name = short.Name
  439. } else {
  440. set := cmd.InheritedFlags()
  441. if short = set.ShorthandLookup(name); short != nil {
  442. name = short.Name
  443. } else {
  444. return nil
  445. }
  446. }
  447. }
  448. return cmd.Flag(name)
  449. }
  450. // CompDebug prints the specified string to the same file as where the
  451. // completion script prints its logs.
  452. // Note that completion printouts should never be on stdout as they would
  453. // be wrongly interpreted as actual completion choices by the completion script.
  454. func CompDebug(msg string, printToStdErr bool) {
  455. msg = fmt.Sprintf("[Debug] %s", msg)
  456. // Such logs are only printed when the user has set the environment
  457. // variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
  458. if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
  459. f, err := os.OpenFile(path,
  460. os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  461. if err == nil {
  462. defer f.Close()
  463. WriteStringAndCheck(f, msg)
  464. }
  465. }
  466. if printToStdErr {
  467. // Must print to stderr for this not to be read by the completion script.
  468. fmt.Fprint(os.Stderr, msg)
  469. }
  470. }
  471. // CompDebugln prints the specified string with a newline at the end
  472. // to the same file as where the completion script prints its logs.
  473. // Such logs are only printed when the user has set the environment
  474. // variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
  475. func CompDebugln(msg string, printToStdErr bool) {
  476. CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
  477. }
  478. // CompError prints the specified completion message to stderr.
  479. func CompError(msg string) {
  480. msg = fmt.Sprintf("[Error] %s", msg)
  481. CompDebug(msg, true)
  482. }
  483. // CompErrorln prints the specified completion message to stderr with a newline at the end.
  484. func CompErrorln(msg string) {
  485. CompError(fmt.Sprintf("%s\n", msg))
  486. }