Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make command suggestion messages configurable #2218

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ type Command struct {
helpCommand *Command
// helpCommandGroupID is the group id for the helpCommand
helpCommandGroupID 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
Expand Down Expand Up @@ -340,6 +342,10 @@ func (c *Command) SetHelpCommandGroupID(groupID string) {
c.helpCommandGroupID = groupID
}

func (c *Command) SetSuggestOutputFunc(f func([]string) string) {
c.suggestOutputFunc = f
}

// SetCompletionCommandGroupID sets the group id of the completion command.
func (c *Command) SetCompletionCommandGroupID(groupID string) {
// completionCommandGroupID is used if no completion command is defined by the user
Expand Down Expand Up @@ -749,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)
Expand Down
120 changes: 120 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,126 @@ func TestSuggestions(t *testing.T) {
}
}

func TestCustomSuggestions(t *testing.T) {
rootCmd := &Command{Use: "root", Run: emptyRun}
timesCmd := &Command{Use: "times", Run: emptyRun}
rootCmd.AddCommand(timesCmd)

var expected, output string

expected = ""
output, _ = executeCommand(rootCmd, "times")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}

expected = fmt.Sprintf("Error: unknown command \"%s\" for \"root\"\n\nDid you mean this?\n\t%s\n\nRun 'root --help' for usage.\n", "time", "times")
output, _ = executeCommand(rootCmd, "time")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}

rootCmd.DisableSuggestions = true

expected = fmt.Sprintf("Error: unknown command \"%s\" for \"root\"\nRun 'root --help' for usage.\n", "time")
output, _ = executeCommand(rootCmd, "time")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}

rootCmd.DisableSuggestions = false
rootCmd.SetSuggestOutputFunc(func(suggestions []string) string {
return fmt.Sprintf("\nSuggestions:\n\t%s\n", strings.Join(suggestions, "\n"))
})

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)
}
}

func TestCustomSuggestions_OnlyValidArgs(t *testing.T) {
validArgs := []string{"a"}
rootCmd := &Command{Use: "root", Args: OnlyValidArgs, Run: emptyRun, ValidArgs: validArgs}
grandparentCmd := &Command{Use: "grandparent", Args: OnlyValidArgs, Run: emptyRun, ValidArgs: validArgs}
parentCmd := &Command{Use: "parent", Args: OnlyValidArgs, Run: emptyRun, ValidArgs: validArgs}
timesCmd := &Command{Use: "times", Run: emptyRun}
parentCmd.AddCommand(timesCmd)
grandparentCmd.AddCommand(parentCmd)
rootCmd.AddCommand(grandparentCmd)

var expected, output string

// No typos.
expected = ""
output, _ = executeCommand(rootCmd, "grandparent")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}

expected = ""
output, _ = executeCommand(rootCmd, "grandparent", "parent")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}

expected = ""
output, _ = executeCommand(rootCmd, "grandparent", "parent", "times")
if output != expected {
t.Errorf("\nExpected:\n %q\nGot:\n %q", expected, output)
}

// 1st level typo.
expected = "Error: invalid argument \"grandparen\" for \"root\"\n\nDid you mean this?\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)
}

// 2nd level typo.
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\"\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.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 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 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.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 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)
}
}

func TestCaseInsensitive(t *testing.T) {
rootCmd := &Command{Use: "root", Run: emptyRun}
childCmd := &Command{Use: "child", Run: emptyRun, Aliases: []string{"alternative"}}
Expand Down
Loading