Skip to content

Commit 8cb30f9

Browse files
feat: add CompletionWithDesc helper (#2231)
The code has also been refactored to use a type alias for completion and a completion helper Using a type alias is a non-breaking change and it makes the code more readable and easier to understand. Signed-off-by: ccoVeille <[email protected]> Co-authored-by: Marc Khouzam <[email protected]>
1 parent 17b6dca commit 8cb30f9

File tree

6 files changed

+118
-46
lines changed

6 files changed

+118
-46
lines changed

active_help.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const (
3535
// This function can be called multiple times before and/or after completions are added to
3636
// the array. Each time this function is called with the same array, the new
3737
// ActiveHelp line will be shown below the previous ones when completion is triggered.
38-
func AppendActiveHelp(compArray []string, activeHelpStr string) []string {
38+
func AppendActiveHelp(compArray []Completion, activeHelpStr string) []Completion {
3939
return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr))
4040
}
4141

command.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ type Command struct {
8282
Example string
8383

8484
// ValidArgs is list of all valid non-flag arguments that are accepted in shell completions
85-
ValidArgs []string
85+
ValidArgs []Completion
8686
// ValidArgsFunction is an optional function that provides valid non-flag arguments for shell completion.
8787
// It is a dynamic version of using ValidArgs.
8888
// Only one of ValidArgs and ValidArgsFunction can be used for a command.
@@ -1272,8 +1272,8 @@ func (c *Command) InitDefaultHelpCmd() {
12721272
Short: "Help about any command",
12731273
Long: `Help provides help for any command in the application.
12741274
Simply type ` + c.DisplayName() + ` help [path to command] for full details.`,
1275-
ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1276-
var completions []string
1275+
ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
1276+
var completions []Completion
12771277
cmd, _, e := c.Root().Find(args)
12781278
if e != nil {
12791279
return nil, ShellCompDirectiveNoFileComp
@@ -1285,7 +1285,7 @@ Simply type ` + c.DisplayName() + ` help [path to command] for full details.`,
12851285
for _, subCmd := range cmd.Commands() {
12861286
if subCmd.IsAvailableCommand() || subCmd == cmd.helpCommand {
12871287
if strings.HasPrefix(subCmd.Name(), toComplete) {
1288-
completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
1288+
completions = append(completions, CompletionWithDesc(subCmd.Name(), subCmd.Short))
12891289
}
12901290
}
12911291
}

completions.go

+37-21
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,31 @@ type CompletionOptions struct {
117117
HiddenDefaultCmd bool
118118
}
119119

120+
// Completion is a string that can be used for completions
121+
//
122+
// two formats are supported:
123+
// - the completion choice
124+
// - the completion choice with a textual description (separated by a TAB).
125+
//
126+
// [CompletionWithDesc] can be used to create a completion string with a textual description.
127+
//
128+
// Note: Go type alias is used to provide a more descriptive name in the documentation, but any string can be used.
129+
type Completion = string
130+
120131
// CompletionFunc is a function that provides completion results.
121-
type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
132+
type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective)
133+
134+
// CompletionWithDesc returns a [Completion] with a description by using the TAB delimited format.
135+
func CompletionWithDesc(choice string, description string) Completion {
136+
return choice + "\t" + description
137+
}
122138

123139
// NoFileCompletions can be used to disable file completion for commands that should
124140
// not trigger file completions.
125141
//
126142
// This method satisfies [CompletionFunc].
127143
// It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
128-
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
144+
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
129145
return nil, ShellCompDirectiveNoFileComp
130146
}
131147

@@ -134,8 +150,8 @@ func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string
134150
//
135151
// This method returns a function that satisfies [CompletionFunc]
136152
// It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
137-
func FixedCompletions(choices []string, directive ShellCompDirective) CompletionFunc {
138-
return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
153+
func FixedCompletions(choices []Completion, directive ShellCompDirective) CompletionFunc {
154+
return func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
139155
return choices, directive
140156
}
141157
}
@@ -290,7 +306,7 @@ type SliceValue interface {
290306
GetSlice() []string
291307
}
292308

293-
func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
309+
func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCompDirective, error) {
294310
// The last argument, which is not completely typed by the user,
295311
// should not be part of the list of arguments
296312
toComplete := args[len(args)-1]
@@ -318,7 +334,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
318334
}
319335
if err != nil {
320336
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
321-
return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
337+
return c, []Completion{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
322338
}
323339
finalCmd.ctx = c.ctx
324340

@@ -348,7 +364,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
348364

349365
// Parse the flags early so we can check if required flags are set
350366
if err = finalCmd.ParseFlags(finalArgs); err != nil {
351-
return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
367+
return finalCmd, []Completion{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
352368
}
353369

354370
realArgCount := finalCmd.Flags().NArg()
@@ -360,14 +376,14 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
360376
if flagErr != nil {
361377
// If error type is flagCompError and we don't want flagCompletion we should ignore the error
362378
if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
363-
return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
379+
return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr
364380
}
365381
}
366382

367383
// Look for the --help or --version flags. If they are present,
368384
// there should be no further completions.
369385
if helpOrVersionFlagPresent(finalCmd) {
370-
return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
386+
return finalCmd, []Completion{}, ShellCompDirectiveNoFileComp, nil
371387
}
372388

373389
// We only remove the flags from the arguments if DisableFlagParsing is not set.
@@ -396,11 +412,11 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
396412
return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
397413
}
398414
// Directory completion
399-
return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
415+
return finalCmd, []Completion{}, ShellCompDirectiveFilterDirs, nil
400416
}
401417
}
402418

403-
var completions []string
419+
var completions []Completion
404420
var directive ShellCompDirective
405421

406422
// Enforce flag groups before doing flag completions
@@ -486,7 +502,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
486502
for _, subCmd := range finalCmd.Commands() {
487503
if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
488504
if strings.HasPrefix(subCmd.Name(), toComplete) {
489-
completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
505+
completions = append(completions, CompletionWithDesc(subCmd.Name(), subCmd.Short))
490506
}
491507
directive = ShellCompDirectiveNoFileComp
492508
}
@@ -542,7 +558,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
542558
if completionFn != nil {
543559
// Go custom completion defined for this flag or command.
544560
// Call the registered completion function to get the completions.
545-
var comps []string
561+
var comps []Completion
546562
comps, directive = completionFn(finalCmd, finalArgs, toComplete)
547563
completions = append(completions, comps...)
548564
}
@@ -562,16 +578,16 @@ func helpOrVersionFlagPresent(cmd *Command) bool {
562578
return false
563579
}
564580

565-
func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
581+
func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []Completion {
566582
if nonCompletableFlag(flag) {
567-
return []string{}
583+
return []Completion{}
568584
}
569585

570-
var completions []string
586+
var completions []Completion
571587
flagName := "--" + flag.Name
572588
if strings.HasPrefix(flagName, toComplete) {
573589
// Flag without the =
574-
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
590+
completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
575591

576592
// Why suggest both long forms: --flag and --flag= ?
577593
// This forces the user to *always* have to type either an = or a space after the flag name.
@@ -583,20 +599,20 @@ func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
583599
// if len(flag.NoOptDefVal) == 0 {
584600
// // Flag requires a value, so it can be suffixed with =
585601
// flagName += "="
586-
// completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
602+
// completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
587603
// }
588604
}
589605

590606
flagName = "-" + flag.Shorthand
591607
if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
592-
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
608+
completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
593609
}
594610

595611
return completions
596612
}
597613

598-
func completeRequireFlags(finalCmd *Command, toComplete string) []string {
599-
var completions []string
614+
func completeRequireFlags(finalCmd *Command, toComplete string) []Completion {
615+
var completions []Completion
600616

601617
doCompleteRequiredFlags := func(flag *pflag.Flag) {
602618
if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {

completions_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -2872,6 +2872,56 @@ func TestFixedCompletions(t *testing.T) {
28722872
}
28732873
}
28742874

2875+
func TestFixedCompletionsWithCompletionHelpers(t *testing.T) {
2876+
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
2877+
// here we are mixing string, [Completion] and [CompletionWithDesc]
2878+
choices := []string{"apple", Completion("banana"), CompletionWithDesc("orange", "orange are orange")}
2879+
childCmd := &Command{
2880+
Use: "child",
2881+
ValidArgsFunction: FixedCompletions(choices, ShellCompDirectiveNoFileComp),
2882+
Run: emptyRun,
2883+
}
2884+
rootCmd.AddCommand(childCmd)
2885+
2886+
t.Run("completion with description", func(t *testing.T) {
2887+
output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "a")
2888+
if err != nil {
2889+
t.Errorf("Unexpected error: %v", err)
2890+
}
2891+
2892+
expected := strings.Join([]string{
2893+
"apple",
2894+
"banana",
2895+
"orange\torange are orange", // this one has the description as expected with [ShellCompRequestCmd] flag
2896+
":4",
2897+
"Completion ended with directive: ShellCompDirectiveNoFileComp", "",
2898+
}, "\n")
2899+
2900+
if output != expected {
2901+
t.Errorf("expected: %q, got: %q", expected, output)
2902+
}
2903+
})
2904+
2905+
t.Run("completion with no description", func(t *testing.T) {
2906+
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "a")
2907+
if err != nil {
2908+
t.Errorf("Unexpected error: %v", err)
2909+
}
2910+
2911+
expected := strings.Join([]string{
2912+
"apple",
2913+
"banana",
2914+
"orange", // the description is absent as expected with [ShellCompNoDescRequestCmd] flag
2915+
":4",
2916+
"Completion ended with directive: ShellCompDirectiveNoFileComp", "",
2917+
}, "\n")
2918+
2919+
if output != expected {
2920+
t.Errorf("expected: %q, got: %q", expected, output)
2921+
}
2922+
})
2923+
}
2924+
28752925
func TestCompletionForGroupedFlags(t *testing.T) {
28762926
getCmd := func() *Command {
28772927
rootCmd := &Command{

site/content/active_help.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ cmd := &cobra.Command{
4141
RunE: func(cmd *cobra.Command, args []string) error {
4242
return addRepo(args)
4343
},
44-
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
45-
var comps []string
44+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
45+
var comps []cobra.Completion
4646
if len(args) == 0 {
4747
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
4848
} else if len(args) == 1 {
@@ -75,7 +75,7 @@ This command does not take any more arguments
7575
Providing Active Help for flags is done in the same fashion as for nouns, but using the completion function registered for the flag. For example:
7676

7777
```go
78-
_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
78+
_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
7979
if len(args) != 2 {
8080
return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp
8181
}
@@ -112,10 +112,10 @@ should or should not be added (instead of reading the environment variable direc
112112
For example:
113113

114114
```go
115-
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
115+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
116116
activeHelpLevel := cobra.GetActiveHelpConfig(cmd)
117117

118-
var comps []string
118+
var comps []cobra.Completion
119119
if len(args) == 0 {
120120
if activeHelpLevel != "off" {
121121
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")

site/content/completions/_index.md

+21-15
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ cmd := &cobra.Command{
177177
RunE: func(cmd *cobra.Command, args []string) {
178178
RunGet(args[0])
179179
},
180-
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
180+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
181181
if len(args) != 0 {
182182
return nil, cobra.ShellCompDirectiveNoFileComp
183183
}
@@ -211,21 +211,21 @@ ShellCompDirectiveNoFileComp
211211

212212
// Indicates that the returned completions should be used as file extension filters.
213213
// For example, to complete only files of the form *.json or *.yaml:
214-
// return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt
214+
// return []cobra.Completion{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
215215
// For flags, using MarkFlagFilename() and MarkPersistentFlagFilename()
216216
// is a shortcut to using this directive explicitly.
217217
//
218218
ShellCompDirectiveFilterFileExt
219219

220220
// Indicates that only directory names should be provided in file completion.
221221
// For example:
222-
// return nil, ShellCompDirectiveFilterDirs
222+
// return nil, cobra.ShellCompDirectiveFilterDirs
223223
// For flags, using MarkFlagDirname() is a shortcut to using this directive explicitly.
224224
//
225225
// To request directory names within another directory, the returned completions
226226
// should specify a single directory name within which to search. For example,
227227
// to complete directories within "themes/":
228-
// return []string{"themes"}, ShellCompDirectiveFilterDirs
228+
// return []cobra.Completion{"themes"}, cobra.ShellCompDirectiveFilterDirs
229229
//
230230
ShellCompDirectiveFilterDirs
231231

@@ -293,8 +293,8 @@ As for nouns, Cobra provides a way of defining dynamic completion of flags. To
293293
294294
```go
295295
flagName := "output"
296-
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
297-
return []string{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault
296+
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
297+
return []cobra.Completion{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault
298298
})
299299
```
300300
Notice that calling `RegisterFlagCompletionFunc()` is done through the `command` with which the flag is associated. In our example this dynamic completion will give results like so:
@@ -327,8 +327,8 @@ cmd.MarkFlagFilename(flagName, "yaml", "json")
327327
or
328328
```go
329329
flagName := "output"
330-
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
331-
return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt})
330+
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
331+
return []cobra.Completion{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt})
332332
```
333333
334334
### Limit flag completions to directory names
@@ -341,15 +341,15 @@ cmd.MarkFlagDirname(flagName)
341341
or
342342
```go
343343
flagName := "output"
344-
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
344+
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
345345
return nil, cobra.ShellCompDirectiveFilterDirs
346346
})
347347
```
348348
To limit completions of flag values to directory names *within another directory* you can use a combination of `RegisterFlagCompletionFunc()` and `ShellCompDirectiveFilterDirs` like so:
349349
```go
350350
flagName := "output"
351-
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
352-
return []string{"themes"}, cobra.ShellCompDirectiveFilterDirs
351+
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
352+
return []cobra.Completion{"themes"}, cobra.ShellCompDirectiveFilterDirs
353353
})
354354
```
355355
### Descriptions for completions
@@ -370,15 +370,21 @@ $ helm s[tab]
370370
search (search for a keyword in charts) show (show information of a chart) status (displays the status of the named release)
371371
```
372372
373-
Cobra allows you to add descriptions to your own completions. Simply add the description text after each completion, following a `\t` separator. This technique applies to completions returned by `ValidArgs`, `ValidArgsFunction` and `RegisterFlagCompletionFunc()`. For example:
373+
Cobra allows you to add descriptions to your own completions. Simply add the description text after each completion, following a `\t` separator. Cobra provides the helper function `CompletionWithDesc(string, string)` to create a completion with a description. This technique applies to completions returned by `ValidArgs`, `ValidArgsFunction` and `RegisterFlagCompletionFunc()`. For example:
374374
```go
375-
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
376-
return []string{"harbor\tAn image registry", "thanos\tLong-term metrics"}, cobra.ShellCompDirectiveNoFileComp
375+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
376+
return []cobra.Completion{
377+
cobra.CompletionWithDesc("harbor", "An image registry"),
378+
cobra.CompletionWithDesc("thanos", "Long-term metrics")
379+
}, cobra.ShellCompDirectiveNoFileComp
377380
}}
378381
```
379382
or
380383
```go
381-
ValidArgs: []string{"bash\tCompletions for bash", "zsh\tCompletions for zsh"}
384+
ValidArgs: []cobra.Completion{
385+
cobra.CompletionWithDesc("bash", "Completions for bash"),
386+
cobra.CompletionWithDesc("zsh", "Completions for zsh")
387+
}
382388
```
383389

384390
If you don't want to show descriptions in the completions, you can add `--no-descriptions` to the default `completion` command to disable them, like:

0 commit comments

Comments
 (0)