Rework to allow override on suggestion output, only, keeping common search logic.

This commit is contained in:
Žan V. Dragan 2025-01-20 21:15:57 +01:00
parent 4d65edfb54
commit 5897ead246
3 changed files with 46 additions and 51 deletions

View file

@ -33,7 +33,7 @@ func legacyArgs(cmd *Command, args []string) error {
// root command with subcommands, do subcommand checking.
if !cmd.HasParent() && len(args) > 0 {
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.SuggestFunc(args[0]))
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
}
return nil
}
@ -58,7 +58,7 @@ func OnlyValidArgs(cmd *Command, args []string) error {
}
for _, v := range args {
if !stringInSlice(v, validArgs) {
return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.SuggestFunc(args[0]))
return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0]))
}
}
}

View file

@ -180,8 +180,8 @@ type Command struct {
helpCommand *Command
// helpCommandGroupID is the group id for the helpCommand
helpCommandGroupID string
// suggestFunc is suggest func defined by the user.
suggestFunc func(string) string
// suggestOutputFunc is user's override for the suggestion output.
suggestOutputFunc func([]string) string
// completionCommandGroupID is the group id for the completion command
completionCommandGroupID string
@ -342,8 +342,8 @@ func (c *Command) SetHelpCommandGroupID(groupID string) {
c.helpCommandGroupID = groupID
}
func (c *Command) SetSuggestFunc(f func(string) string) {
c.suggestFunc = f
func (c *Command) SetSuggestOutputFunc(f func([]string) string) {
c.suggestOutputFunc = f
}
// SetCompletionCommandGroupID sets the group id of the completion command.
@ -483,37 +483,6 @@ func (c *Command) Help() error {
return nil
}
// SuggestFunc returns suggestions for the provided typedName using either
// the function set by SetSuggestFunc for this command, parent's or a default one.
// When searching for a parent's function, it recursively checks towards the root
// and returns the first one found. If none found, uses direct parent's default.
func (c *Command) SuggestFunc(typedName string) string {
if c.DisableSuggestions {
return ""
}
if c.suggestFunc != nil {
return c.suggestFunc(typedName)
}
if c.HasParent() {
var getParentFunc func(*Command) func(string) string
getParentFunc = func(parent *Command) func(string) string {
if parent.suggestFunc != nil {
return parent.suggestFunc
}
if !parent.HasParent() {
return nil
}
return getParentFunc(parent.Parent())
}
parentFunc := getParentFunc(c.Parent())
if parentFunc != nil {
return parentFunc(typedName)
}
return c.Parent().findSuggestions(typedName)
}
return c.findSuggestions(typedName)
}
// UsageString returns usage string.
func (c *Command) UsageString() string {
// Storing normal writers
@ -786,15 +755,41 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
return commandFound, a, nil
}
func (c *Command) findSuggestions(arg string) string {
// findSuggestions returns suggestions for the provided typedName if suggestions aren't disabled.
// The output building function can be overridden by setting it with the SetSuggestOutputFunc.
// If the output override is, instead, set on a parent, it uses the first one found.
// If none is set, a default is used.
func (c *Command) findSuggestions(typedName string) string {
if c.DisableSuggestions {
return ""
}
if c.SuggestionsMinimumDistance <= 0 {
c.SuggestionsMinimumDistance = 2
}
suggestions := c.SuggestionsFor(typedName)
if c.suggestOutputFunc != nil {
return c.suggestOutputFunc(suggestions)
}
if c.HasParent() {
var getParentFunc func(*Command) func([]string) string
getParentFunc = func(parent *Command) func([]string) string {
if parent.suggestOutputFunc != nil {
return parent.suggestOutputFunc
}
if !parent.HasParent() {
return nil
}
return getParentFunc(parent.Parent())
}
if parentFunc := getParentFunc(c.Parent()); parentFunc != nil {
return parentFunc(suggestions)
}
}
var sb strings.Builder
if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 {
if len(suggestions) > 0 {
sb.WriteString("\n\nDid you mean this?\n")
for _, s := range suggestions {
_, _ = fmt.Fprintf(&sb, "\t%v\n", s)

View file

@ -1421,11 +1421,11 @@ func TestCustomSuggestions(t *testing.T) {
}
rootCmd.DisableSuggestions = false
rootCmd.SetSuggestFunc(func(typedName string) string {
return "\nSome custom suggestion.\n"
rootCmd.SetSuggestOutputFunc(func(suggestions []string) string {
return fmt.Sprintf("\nSuggestions:\n\t%s\n", strings.Join(suggestions, "\n"))
})
expected = fmt.Sprintf("Error: unknown command \"%s\" for \"root\"\nSome custom suggestion.\n\nRun 'root --help' for usage.\n", "time")
expected = fmt.Sprintf("Error: unknown command \"time\" for \"root\"\nSuggestions:\n\ttimes\n\nRun 'root --help' for usage.\n")
output, _ = executeCommand(rootCmd, "time")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
@ -1471,42 +1471,42 @@ func TestCustomSuggestions_OnlyValidArgs(t *testing.T) {
}
// 2nd level typo.
expected = "Error: invalid argument \"paren\" for \"root grandparent\"\nUsage:\n root grandparent [flags]\n root grandparent [command]\n\nAvailable Commands:\n parent \n\nFlags:\n -h, --help help for grandparent\n\nUse \"root grandparent [command] --help\" for more information about a command.\n\n"
expected = "Error: invalid argument \"paren\" for \"root grandparent\"\n\nDid you mean this?\n\tparent\n\nUsage:\n root grandparent [flags]\n root grandparent [command]\n\nAvailable Commands:\n parent \n\nFlags:\n -h, --help help for grandparent\n\nUse \"root grandparent [command] --help\" for more information about a command.\n\n"
output, _ = executeCommand(rootCmd, "grandparent", "paren")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}
// 3rd level typo.
expected = "Error: invalid argument \"time\" for \"root grandparent parent\"\nUsage:\n root grandparent parent [flags]\n root grandparent parent [command]\n\nAvailable Commands:\n times \n\nFlags:\n -h, --help help for parent\n\nUse \"root grandparent parent [command] --help\" for more information about a command.\n\n"
expected = "Error: invalid argument \"time\" for \"root grandparent parent\"\n\nDid you mean this?\n\ttimes\n\nUsage:\n root grandparent parent [flags]\n root grandparent parent [command]\n\nAvailable Commands:\n times \n\nFlags:\n -h, --help help for parent\n\nUse \"root grandparent parent [command] --help\" for more information about a command.\n\n"
output, _ = executeCommand(rootCmd, "grandparent", "parent", "time")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}
// Custom suggestion on root function.
rootCmd.SetSuggestFunc(func(typedName string) string {
return "\nRoot custom suggestion.\n"
rootCmd.SetSuggestOutputFunc(func(suggestions []string) string {
return fmt.Sprintf("\nRoot Suggestions:\n\t%s\n", strings.Join(suggestions, "\n"))
})
expected = "Error: invalid argument \"grandparen\" for \"root\"\nRoot custom suggestion.\n\nUsage:\n root [flags]\n root [command]\n\nAvailable Commands:\n completion Generate the autocompletion script for the specified shell\n grandparent \n help Help about any command\n\nFlags:\n -h, --help help for root\n\nUse \"root [command] --help\" for more information about a command.\n\n"
expected = "Error: invalid argument \"grandparen\" for \"root\"\nRoot Suggestions:\n\tgrandparent\n\nUsage:\n root [flags]\n root [command]\n\nAvailable Commands:\n completion Generate the autocompletion script for the specified shell\n grandparent \n help Help about any command\n\nFlags:\n -h, --help help for root\n\nUse \"root [command] --help\" for more information about a command.\n\n"
output, _ = executeCommand(rootCmd, "grandparen")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}
expected = "Error: invalid argument \"time\" for \"root grandparent parent\"\nRoot custom suggestion.\n\nUsage:\n root grandparent parent [flags]\n root grandparent parent [command]\n\nAvailable Commands:\n times \n\nFlags:\n -h, --help help for parent\n\nUse \"root grandparent parent [command] --help\" for more information about a command.\n\n"
expected = "Error: invalid argument \"time\" for \"root grandparent parent\"\nRoot Suggestions:\n\ttimes\n\nUsage:\n root grandparent parent [flags]\n root grandparent parent [command]\n\nAvailable Commands:\n times \n\nFlags:\n -h, --help help for parent\n\nUse \"root grandparent parent [command] --help\" for more information about a command.\n\n"
output, _ = executeCommand(rootCmd, "grandparent", "parent", "time")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}
// Custom suggestion on parent function (kept root's to make sure this one is prioritised).
parentCmd.SetSuggestFunc(func(typedName string) string {
return "\nParent custom suggestion.\n"
parentCmd.SetSuggestOutputFunc(func(suggestions []string) string {
return fmt.Sprintf("\nParent Suggestions:\n\t%s\n", strings.Join(suggestions, "\n"))
})
expected = "Error: invalid argument \"time\" for \"root grandparent parent\"\nParent custom suggestion.\n\nUsage:\n root grandparent parent [flags]\n root grandparent parent [command]\n\nAvailable Commands:\n times \n\nFlags:\n -h, --help help for parent\n\nUse \"root grandparent parent [command] --help\" for more information about a command.\n\n"
expected = "Error: invalid argument \"time\" for \"root grandparent parent\"\nParent Suggestions:\n\ttimes\n\nUsage:\n root grandparent parent [flags]\n root grandparent parent [command]\n\nAvailable Commands:\n times \n\nFlags:\n -h, --help help for parent\n\nUse \"root grandparent parent [command] --help\" for more information about a command.\n\n"
output, _ = executeCommand(rootCmd, "grandparent", "parent", "time")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)