From f80a6ab97a2ec30fecc60e91f48d40eacd1bd0e9 Mon Sep 17 00:00:00 2001 From: ccoVeille <3875889+ccoVeille@users.noreply.github.com> Date: Sat, 8 Feb 2025 02:00:26 +0100 Subject: [PATCH] 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> --- command.go | 4 ++-- completions.go | 50 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/command.go b/command.go index 60b9c455..d108f856 100644 --- a/command.go +++ b/command.go @@ -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 []CompletionChoice // 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. @@ -94,7 +94,7 @@ type Command struct { // ArgAliases is List of aliases for ValidArgs. // These are not suggested to the user in the shell completion, // but accepted if entered manually. - ArgAliases []string + ArgAliases []CompletionChoice // BashCompletionFunction is custom bash functions used by the legacy bash autocompletion generator. // For portability with other shells, it is recommended to instead use ValidArgsFunction diff --git a/completions.go b/completions.go index cd899c73..044a83e7 100644 --- a/completions.go +++ b/completions.go @@ -117,15 +117,31 @@ type CompletionOptions struct { HiddenDefaultCmd bool } +// CompletionChoice is a string that can be used for completions +// +// two formats are supported: +// - the name of the flag +// - the name of the flag with its description (separated by a tab) +// +// [CompletionChoiceWithDescription] can be used to create a completion string with a description using the TAB separator. +// +// Note: Go type alias is used to provide a more descriptive name in the documentation, but any string can be used. +type CompletionChoice = 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 []CompletionChoice, toComplete string) ([]string, ShellCompDirective) + +// CompletionChoiceWithDescription returns a CompletionChoice with a description +func CompletionChoiceWithDescription(name, desc string) CompletionChoice { + return name + "\t" + desc +} // 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 []CompletionChoice, toComplete string) ([]string, 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 []CompletionChoice, directive ShellCompDirective) CompletionFunc { + return func(cmd *Command, args []CompletionChoice, toComplete string) ([]string, 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, []CompletionChoice, 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, []CompletionChoice{}, 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, []CompletionChoice{}, 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, []CompletionChoice{}, 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, []CompletionChoice{}, 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, []CompletionChoice{}, ShellCompDirectiveFilterDirs, nil } } - var completions []string + var completions []CompletionChoice var directive ShellCompDirective // Enforce flag groups before doing flag completions @@ -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 []CompletionChoice comps, directive = completionFn(finalCmd, finalArgs, toComplete) completions = append(completions, comps...) } @@ -562,12 +578,12 @@ func helpOrVersionFlagPresent(cmd *Command) bool { return false } -func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string { +func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []CompletionChoice { if nonCompletableFlag(flag) { - return []string{} + return []CompletionChoice{} } - var completions []string + var completions []CompletionChoice flagName := "--" + flag.Name if strings.HasPrefix(flagName, toComplete) { // Flag without the = @@ -595,8 +611,8 @@ func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string { return completions } -func completeRequireFlags(finalCmd *Command, toComplete string) []string { - var completions []string +func completeRequireFlags(finalCmd *Command, toComplete string) []CompletionChoice { + var completions []CompletionChoice doCompleteRequiredFlags := func(flag *pflag.Flag) { if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {