Support subcommands checking for suggestions

Till now, only the root command is looked for suggestions.
Now `findSuggestions` works for nested commands as well.

For example

```
Error: unknown command "rewin" for "root times"

Did you mean this?
	rewind

Run 'root times --help' for usage.
```

Signed-off-by: Yuval Goldberg <yuvigoldi@gmail.com>
This commit is contained in:
Yuval Goldberg 2021-10-04 01:36:57 +03:00
parent 4fd30b69ee
commit bbadd15b86
2 changed files with 98 additions and 26 deletions

View file

@ -8,8 +8,8 @@ import (
type PositionalArgs func(cmd *Command, args []string) error type PositionalArgs func(cmd *Command, args []string) error
// Legacy arg validation has the following behaviour: // Legacy arg validation has the following behaviour:
// - root commands with no subcommands can take arbitrary arguments // - commands with no subcommands can take arbitrary arguments
// - root commands with subcommands will do subcommand validity checking // - commands with subcommands will do subcommand validity checking
// - subcommands will always accept arbitrary arguments // - subcommands will always accept arbitrary arguments
func legacyArgs(cmd *Command, args []string) error { func legacyArgs(cmd *Command, args []string) error {
// no subcommand, always take args // no subcommand, always take args
@ -17,8 +17,8 @@ func legacyArgs(cmd *Command, args []string) error {
return nil return nil
} }
// root command with subcommands, do subcommand checking. // do subcommand checking
if !cmd.HasParent() && len(args) > 0 { if len(args) > 0 {
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
} }
return nil return nil

View file

@ -1194,39 +1194,111 @@ func TestSuggestions(t *testing.T) {
SuggestFor: []string{"counts"}, SuggestFor: []string{"counts"},
Run: emptyRun, Run: emptyRun,
} }
rewindCmd := &Command{
Use: "rewind",
SuggestFor: []string{"dejavu"},
Run: emptyRun,
}
timesCmd.AddCommand(rewindCmd)
rootCmd.AddCommand(timesCmd) rootCmd.AddCommand(timesCmd)
templateWithSuggestions := "Error: unknown command \"%s\" for \"root\"\n\nDid you mean this?\n\t%s\n\nRun 'root --help' for usage.\n" templateSuggestions := "\nDid you mean this?\n\t%s\n\n"
templateWithoutSuggestions := "Error: unknown command \"%s\" for \"root\"\nRun 'root --help' for usage.\n"
tests := map[string]string{ templatePrefix := "Error: unknown command \"%s\" for \"%s\"\n"
"time": "times", templateSuffix := "Run '%s --help' for usage.\n"
"tiems": "times",
"tims": "times", tests := []struct {
"timeS": "times", input string
"rimes": "times", targetCommand string
"ti": "times", expectedSuggestion string
"t": "times", }{
"timely": "times", {
"ri": "", input: "time",
"timezone": "", expectedSuggestion: "times",
"foo": "", },
"counts": "times", {
input: "tiems",
expectedSuggestion: "times",
},
{
input: "tims",
expectedSuggestion: "times",
},
{
input: "timeS",
expectedSuggestion: "times",
},
{
input: "rimes",
expectedSuggestion: "times",
},
{
input: "ti",
expectedSuggestion: "times",
},
{
input: "t",
expectedSuggestion: "times",
},
{
input: "timely",
expectedSuggestion: "times",
},
{
input: "ri",
expectedSuggestion: "",
},
{
input: "timezone",
expectedSuggestion: "",
},
{
input: "foo",
expectedSuggestion: "",
},
{
input: "counts",
expectedSuggestion: "times",
},
{
input: "foo rewind",
expectedSuggestion: "",
},
{
input: "times rewin",
targetCommand: "root times",
expectedSuggestion: "rewind",
},
{
input: "times dejavu",
targetCommand: "root times",
expectedSuggestion: "rewind",
},
} }
for typo, suggestion := range tests { for index := range tests {
for _, suggestionsDisabled := range []bool{true, false} { for _, suggestionsDisabled := range []bool{true, false} {
test := tests[index]
timesCmd.DisableSuggestions = suggestionsDisabled
rootCmd.DisableSuggestions = suggestionsDisabled rootCmd.DisableSuggestions = suggestionsDisabled
var expected string args := strings.Split(test.input, " ")
output, _ := executeCommand(rootCmd, typo) output, _ := executeCommand(rootCmd, args...)
if suggestion == "" || suggestionsDisabled { unknownArg := args[len(args)-1]
expected = fmt.Sprintf(templateWithoutSuggestions, typo) if test.targetCommand == "" {
} else { test.targetCommand = rootCmd.Use
expected = fmt.Sprintf(templateWithSuggestions, typo, suggestion) unknownArg = args[0]
} }
expected := fmt.Sprintf(templatePrefix, unknownArg, test.targetCommand)
if test.expectedSuggestion != "" && !suggestionsDisabled {
expected += fmt.Sprintf(templateSuggestions, test.expectedSuggestion)
}
expected += fmt.Sprintf(templateSuffix, test.targetCommand)
if output != expected { if output != expected {
t.Errorf("Unexpected response.\nExpected:\n %q\nGot:\n %q\n", expected, output) t.Errorf("Unexpected response.\nExpected:\n %q\nGot:\n %q\n", expected, output)
} }