1
0
Fork 0
mirror of https://github.com/spf13/cobra synced 2025-04-01 04:19:15 +00:00

feat: add CompletionWithDesc helper ()

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 <3875889+ccoVeille@users.noreply.github.com>
Co-authored-by: Marc Khouzam <marc.khouzam@gmail.com>
This commit is contained in:
ccoVeille 2025-02-09 17:06:07 +01:00 committed by GitHub
parent 17b6dca2ff
commit 8cb30f9ca5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 46 deletions

View file

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

View file

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

View file

@ -117,15 +117,31 @@ type CompletionOptions struct {
HiddenDefaultCmd bool
}
// Completion is a string that can be used for completions
//
// two formats are supported:
// - the completion choice
// - the completion choice with a textual description (separated by a TAB).
//
// [CompletionWithDesc] can be used to create a completion string with a textual description.
//
// Note: Go type alias is used to provide a more descriptive name in the documentation, but any string can be used.
type Completion = string
// CompletionFunc is a function that provides completion results.
type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective)
// CompletionWithDesc returns a [Completion] with a description by using the TAB delimited format.
func CompletionWithDesc(choice string, description string) Completion {
return choice + "\t" + description
}
// NoFileCompletions can be used to disable file completion for commands that should
// not trigger file completions.
//
// This method satisfies [CompletionFunc].
// It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
return nil, ShellCompDirectiveNoFileComp
}
@ -134,8 +150,8 @@ func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string
//
// This method returns a function that satisfies [CompletionFunc]
// It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
func FixedCompletions(choices []string, directive ShellCompDirective) CompletionFunc {
return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
func FixedCompletions(choices []Completion, directive ShellCompDirective) CompletionFunc {
return func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
return choices, directive
}
}
@ -290,7 +306,7 @@ type SliceValue interface {
GetSlice() []string
}
func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCompDirective, error) {
// The last argument, which is not completely typed by the user,
// should not be part of the list of arguments
toComplete := args[len(args)-1]
@ -318,7 +334,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
}
if err != nil {
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
return c, []Completion{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
}
finalCmd.ctx = c.ctx
@ -348,7 +364,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
// Parse the flags early so we can check if required flags are set
if err = finalCmd.ParseFlags(finalArgs); err != nil {
return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
return finalCmd, []Completion{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
}
realArgCount := finalCmd.Flags().NArg()
@ -360,14 +376,14 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
if flagErr != nil {
// If error type is flagCompError and we don't want flagCompletion we should ignore the error
if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr
}
}
// Look for the --help or --version flags. If they are present,
// there should be no further completions.
if helpOrVersionFlagPresent(finalCmd) {
return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
return finalCmd, []Completion{}, ShellCompDirectiveNoFileComp, nil
}
// 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
return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
}
// Directory completion
return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
return finalCmd, []Completion{}, ShellCompDirectiveFilterDirs, nil
}
}
var completions []string
var completions []Completion
var directive ShellCompDirective
// Enforce flag groups before doing flag completions
@ -486,7 +502,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
for _, subCmd := range finalCmd.Commands() {
if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
if strings.HasPrefix(subCmd.Name(), toComplete) {
completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
completions = append(completions, CompletionWithDesc(subCmd.Name(), subCmd.Short))
}
directive = ShellCompDirectiveNoFileComp
}
@ -542,7 +558,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
if completionFn != nil {
// Go custom completion defined for this flag or command.
// Call the registered completion function to get the completions.
var comps []string
var comps []Completion
comps, directive = completionFn(finalCmd, finalArgs, toComplete)
completions = append(completions, comps...)
}
@ -562,16 +578,16 @@ func helpOrVersionFlagPresent(cmd *Command) bool {
return false
}
func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []Completion {
if nonCompletableFlag(flag) {
return []string{}
return []Completion{}
}
var completions []string
var completions []Completion
flagName := "--" + flag.Name
if strings.HasPrefix(flagName, toComplete) {
// Flag without the =
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
// Why suggest both long forms: --flag and --flag= ?
// 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 {
// if len(flag.NoOptDefVal) == 0 {
// // Flag requires a value, so it can be suffixed with =
// flagName += "="
// completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
// completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
// }
}
flagName = "-" + flag.Shorthand
if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
}
return completions
}
func completeRequireFlags(finalCmd *Command, toComplete string) []string {
var completions []string
func completeRequireFlags(finalCmd *Command, toComplete string) []Completion {
var completions []Completion
doCompleteRequiredFlags := func(flag *pflag.Flag) {
if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {

View file

@ -2872,6 +2872,56 @@ func TestFixedCompletions(t *testing.T) {
}
}
func TestFixedCompletionsWithCompletionHelpers(t *testing.T) {
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
// here we are mixing string, [Completion] and [CompletionWithDesc]
choices := []string{"apple", Completion("banana"), CompletionWithDesc("orange", "orange are orange")}
childCmd := &Command{
Use: "child",
ValidArgsFunction: FixedCompletions(choices, ShellCompDirectiveNoFileComp),
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)
t.Run("completion with description", func(t *testing.T) {
output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "a")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"apple",
"banana",
"orange\torange are orange", // this one has the description as expected with [ShellCompRequestCmd] flag
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", "",
}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
})
t.Run("completion with no description", func(t *testing.T) {
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "a")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"apple",
"banana",
"orange", // the description is absent as expected with [ShellCompNoDescRequestCmd] flag
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", "",
}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
})
}
func TestCompletionForGroupedFlags(t *testing.T) {
getCmd := func() *Command {
rootCmd := &Command{

View file

@ -41,8 +41,8 @@ cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
return addRepo(args)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var comps []string
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
var comps []cobra.Completion
if len(args) == 0 {
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
} else if len(args) == 1 {
@ -75,7 +75,7 @@ This command does not take any more arguments
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:
```go
_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) != 2 {
return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp
}
@ -112,10 +112,10 @@ should or should not be added (instead of reading the environment variable direc
For example:
```go
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
activeHelpLevel := cobra.GetActiveHelpConfig(cmd)
var comps []string
var comps []cobra.Completion
if len(args) == 0 {
if activeHelpLevel != "off" {
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")

View file

@ -177,7 +177,7 @@ cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) {
RunGet(args[0])
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
@ -211,7 +211,7 @@ ShellCompDirectiveNoFileComp
// Indicates that the returned completions should be used as file extension filters.
// For example, to complete only files of the form *.json or *.yaml:
// return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt
// return []cobra.Completion{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
// For flags, using MarkFlagFilename() and MarkPersistentFlagFilename()
// is a shortcut to using this directive explicitly.
//
@ -219,13 +219,13 @@ ShellCompDirectiveFilterFileExt
// Indicates that only directory names should be provided in file completion.
// For example:
// return nil, ShellCompDirectiveFilterDirs
// return nil, cobra.ShellCompDirectiveFilterDirs
// For flags, using MarkFlagDirname() is a shortcut to using this directive explicitly.
//
// To request directory names within another directory, the returned completions
// should specify a single directory name within which to search. For example,
// to complete directories within "themes/":
// return []string{"themes"}, ShellCompDirectiveFilterDirs
// return []cobra.Completion{"themes"}, cobra.ShellCompDirectiveFilterDirs
//
ShellCompDirectiveFilterDirs
@ -293,8 +293,8 @@ As for nouns, Cobra provides a way of defining dynamic completion of flags. To
```go
flagName := "output"
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return []cobra.Completion{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault
})
```
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")
or
```go
flagName := "output"
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt})
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return []cobra.Completion{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt})
```
### Limit flag completions to directory names
@ -341,15 +341,15 @@ cmd.MarkFlagDirname(flagName)
or
```go
flagName := "output"
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterDirs
})
```
To limit completions of flag values to directory names *within another directory* you can use a combination of `RegisterFlagCompletionFunc()` and `ShellCompDirectiveFilterDirs` like so:
```go
flagName := "output"
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"themes"}, cobra.ShellCompDirectiveFilterDirs
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return []cobra.Completion{"themes"}, cobra.ShellCompDirectiveFilterDirs
})
```
### Descriptions for completions
@ -370,15 +370,21 @@ $ helm s[tab]
search (search for a keyword in charts) show (show information of a chart) status (displays the status of the named release)
```
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:
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:
```go
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"harbor\tAn image registry", "thanos\tLong-term metrics"}, cobra.ShellCompDirectiveNoFileComp
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return []cobra.Completion{
cobra.CompletionWithDesc("harbor", "An image registry"),
cobra.CompletionWithDesc("thanos", "Long-term metrics")
}, cobra.ShellCompDirectiveNoFileComp
}}
```
or
```go
ValidArgs: []string{"bash\tCompletions for bash", "zsh\tCompletions for zsh"}
ValidArgs: []cobra.Completion{
cobra.CompletionWithDesc("bash", "Completions for bash"),
cobra.CompletionWithDesc("zsh", "Completions for zsh")
}
```
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: