From ba6c848587a35324bf15461e1fea074f9203827f Mon Sep 17 00:00:00 2001 From: Yuval Goldberg Date: Mon, 4 Oct 2021 01:36:57 +0300 Subject: [PATCH] Support subcommands checking for suggestions (#1500) 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 --- args.go | 8 ++-- command_test.go | 122 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/args.go b/args.go index aad963bd10..e9a7e15572 100644 --- a/args.go +++ b/args.go @@ -27,8 +27,8 @@ func validateArgs(cmd *Command, args []string) error { } // Legacy arg validation has the following behaviour: -// - root commands with no subcommands can take arbitrary arguments -// - root commands with subcommands will do subcommand validity checking +// - commands with no subcommands can take arbitrary arguments +// - commands with subcommands will do subcommand validity checking // - subcommands will always accept arbitrary arguments func legacyArgs(cmd *Command, args []string) error { // no subcommand, always take args @@ -36,8 +36,8 @@ func legacyArgs(cmd *Command, args []string) error { return nil } - // root command with subcommands, do subcommand checking. - if !cmd.HasParent() && len(args) > 0 { + // do subcommand checking + if len(args) > 0 { return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) } return nil diff --git a/command_test.go b/command_test.go index 583cb02352..9a566d875c 100644 --- a/command_test.go +++ b/command_test.go @@ -1194,39 +1194,111 @@ func TestSuggestions(t *testing.T) { SuggestFor: []string{"counts"}, Run: emptyRun, } + rewindCmd := &Command{ + Use: "rewind", + SuggestFor: []string{"dejavu"}, + Run: emptyRun, + } + timesCmd.AddCommand(rewindCmd) 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" - templateWithoutSuggestions := "Error: unknown command \"%s\" for \"root\"\nRun 'root --help' for usage.\n" - - tests := map[string]string{ - "time": "times", - "tiems": "times", - "tims": "times", - "timeS": "times", - "rimes": "times", - "ti": "times", - "t": "times", - "timely": "times", - "ri": "", - "timezone": "", - "foo": "", - "counts": "times", - } - - for typo, suggestion := range tests { + templateSuggestions := "\nDid you mean this?\n\t%s\n\n" + + templatePrefix := "Error: unknown command \"%s\" for \"%s\"\n" + templateSuffix := "Run '%s --help' for usage.\n" + + tests := []struct { + input string + targetCommand string + expectedSuggestion string + }{ + { + input: "time", + expectedSuggestion: "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 index := range tests { for _, suggestionsDisabled := range []bool{true, false} { + test := tests[index] + + timesCmd.DisableSuggestions = suggestionsDisabled rootCmd.DisableSuggestions = suggestionsDisabled - var expected string - output, _ := executeCommand(rootCmd, typo) + args := strings.Split(test.input, " ") + output, _ := executeCommand(rootCmd, args...) - if suggestion == "" || suggestionsDisabled { - expected = fmt.Sprintf(templateWithoutSuggestions, typo) - } else { - expected = fmt.Sprintf(templateWithSuggestions, typo, suggestion) + unknownArg := args[len(args)-1] + if test.targetCommand == "" { + test.targetCommand = rootCmd.Use + 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 { t.Errorf("Unexpected response.\nExpected:\n %q\nGot:\n %q\n", expected, output) }