From d89c499964451591c4e8142464f0d558c4a3698d Mon Sep 17 00:00:00 2001 From: Eric Paris Date: Tue, 9 Jun 2015 14:17:58 -0400 Subject: [PATCH] Allow commands to explicitly state if they do, or do not take arbitrary arguments Check that arguments are in ValidArgs If a command defined cmd.ValidArgs check that the argument is actually in ValidArgs and fail if it is not. --- README.md | 32 +++++++++++++++++ bash_completions_test.go | 2 ++ cobra_test.go | 69 +++++++++++++++++++++++++++++++++--- command.go | 76 +++++++++++++++++++++++++++++++--------- 4 files changed, 157 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e249c1bc..02075833 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,38 @@ A flag can also be assigned locally which will only apply to that specific comma RootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from") ``` +### Specify if you command takes arguments + +There are multiple options for how a command can handle unknown arguments which can be set in `TakesArgs` +- `Legacy` +- `None` +- `Arbitrary` +- `ValidOnly` + +`Legacy` (or default) the rules are as follows: +- root commands with no subcommands can take arbitrary arguments +- root commands with subcommands will do subcommand validity checking +- subcommands will always accept arbitrary arguments and do no subsubcommand validity checking + +`None` the command will be rejected if there are any left over arguments after parsing flags. + +`Arbitrary` any additional values left after parsing flags will be passed in to your `Run` function. + +`ValidOnly` you must define all valid (non-subcommand) arguments to your command. These are defined in a slice name ValidArgs. For example a command which only takes the argument "one" or "two" would be defined as: + +```go +var HugoCmd = &cobra.Command{ + Use: "hugo", + Short: "Hugo is a very fast static site generator", + ValidArgs: []string{"one", "two", "three", "four"} + TakesArgs: cobra.ValidOnly + Run: func(cmd *cobra.Command, args []string) { + // args will only have the values one, two, three, four + // or the cmd.Execute() will fail. + }, + } +``` + ### Bind Flags with Config You can also bind your flags with [viper](https://github.com/spf13/viper): diff --git a/bash_completions_test.go b/bash_completions_test.go index 7511376a..071a6a2a 100644 --- a/bash_completions_test.go +++ b/bash_completions_test.go @@ -117,6 +117,8 @@ func TestBashCompletions(t *testing.T) { // check for filename extension flags check(t, str, `flags_completion+=("_filedir")`) // check for filename extension flags + check(t, str, `must_have_one_noun+=("three")`) + // check for filename extention flags check(t, str, `flags_completion+=("__handle_filename_extension_flag json|yaml|yml")`) // check for custom flags check(t, str, `flags_completion+=("__complete_custom")`) diff --git a/cobra_test.go b/cobra_test.go index 576c97d3..89dfb3c5 100644 --- a/cobra_test.go +++ b/cobra_test.go @@ -75,6 +75,7 @@ var cmdDeprecated = &Command{ Deprecated: "Please use echo instead", Run: func(cmd *Command, args []string) { }, + TakesArgs: None, } var cmdTimes = &Command{ @@ -88,6 +89,8 @@ var cmdTimes = &Command{ Run: func(cmd *Command, args []string) { tt = args }, + TakesArgs: ValidOnly, + ValidArgs: []string{"one", "two", "three", "four"}, } var cmdRootNoRun = &Command{ @@ -100,9 +103,20 @@ var cmdRootNoRun = &Command{ } var cmdRootSameName = &Command{ - Use: "print", - Short: "Root with the same name as a subcommand", - Long: "The root description for help", + Use: "print", + Short: "Root with the same name as a subcommand", + Long: "The root description for help", + TakesArgs: None, +} + +var cmdRootTakesArgs = &Command{ + Use: "root-with-args [random args]", + Short: "The root can run it's own function and takes args!", + Long: "The root description for help, and some args", + Run: func(cmd *Command, args []string) { + tr = args + }, + TakesArgs: Arbitrary, } var cmdRootWithRun = &Command{ @@ -458,6 +472,51 @@ func TestUsage(t *testing.T) { checkResultOmits(t, x, cmdCustomFlags.Use+" [flags]") } +func TestRootTakesNoArgs(t *testing.T) { + c := initializeWithSameName() + c.AddCommand(cmdPrint, cmdEcho) + result := simpleTester(c, "illegal") + + expectedError := `unknown command "illegal" for "print"` + if !strings.Contains(result.Error.Error(), expectedError) { + t.Errorf("exptected %v, got %v", expectedError, result.Error.Error()) + } +} + +func TestRootTakesArgs(t *testing.T) { + c := cmdRootTakesArgs + result := simpleTester(c, "legal") + + if result.Error != nil { + t.Errorf("expected no error, but got %v", result.Error) + } +} + +func TestSubCmdTakesNoArgs(t *testing.T) { + result := fullSetupTest("deprecated illegal") + + expectedError := `unknown command "illegal" for "cobra-test deprecated"` + if !strings.Contains(result.Error.Error(), expectedError) { + t.Errorf("expected %v, got %v", expectedError, result.Error.Error()) + } +} + +func TestSubCmdTakesArgs(t *testing.T) { + noRRSetupTest("echo times one two") + if strings.Join(tt, " ") != "one two" { + t.Error("Command didn't parse correctly") + } +} + +func TestCmdOnlyValidArgs(t *testing.T) { + result := noRRSetupTest("echo times one two five") + + expectedError := `invalid argument "five"` + if !strings.Contains(result.Error.Error(), expectedError) { + t.Errorf("expected %v, got %v", expectedError, result.Error.Error()) + } +} + func TestFlagLong(t *testing.T) { noRRSetupTest("echo", "--intone=13", "something", "--", "here") @@ -672,9 +731,9 @@ func TestPersistentFlags(t *testing.T) { } // persistentFlag should act like normal flag on its own command - fullSetupTest("echo", "times", "-s", "again", "-c", "-p", "test", "here") + fullSetupTest("echo", "times", "-s", "again", "-c", "-p", "one", "two") - if strings.Join(tt, " ") != "test here" { + if strings.Join(tt, " ") != "one two" { t.Errorf("flags didn't leave proper args remaining. %s given", tt) } diff --git a/command.go b/command.go index c3e16b1d..131f01c4 100644 --- a/command.go +++ b/command.go @@ -27,6 +27,15 @@ import ( flag "github.com/spf13/pflag" ) +type Args int + +const ( + Legacy Args = iota + Arbitrary + ValidOnly + None +) + // Command is just that, a command for your application. // E.g. 'go run ...' - 'run' is the command. Cobra requires // you to define the usage and description as part of your command @@ -59,6 +68,8 @@ type Command struct { // but accepted if entered manually. ArgAliases []string + // Does this command take arbitrary arguments + TakesArgs Args // BashCompletionFunction is custom functions used by the bash autocompletion generator. BashCompletionFunction string @@ -472,6 +483,15 @@ func argsMinusFirstX(args []string, x string) []string { return args } +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + // Find the target command given the args and command tree // Meant to be run on the highest node. Only searches down. func (c *Command) Find(args []string) (*Command, []string, error) { @@ -515,31 +535,53 @@ func (c *Command) Find(args []string) (*Command, []string, error) { commandFound, a := innerfind(c, args) argsWOflags := stripFlags(a, commandFound) - // no subcommand, always take args - if !commandFound.HasSubCommands() { + // "Legacy" has some 'odd' characteristics. + // - root commands with no subcommands can take arbitrary arguments + // - root commands with subcommands will do subcommand validity checking + // - subcommands will always accept arbitrary arguments + if commandFound.TakesArgs == Legacy { + // no subcommand, always take args + if !commandFound.HasSubCommands() { + return commandFound, a, nil + } + // root command with subcommands, do subcommand checking + if commandFound == c && len(argsWOflags) > 0 { + return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), c.findSuggestions(argsWOflags)) + } return commandFound, a, nil } - // root command with subcommands, do subcommand checking - if commandFound == c && len(argsWOflags) > 0 { - suggestionsString := "" - if !c.DisableSuggestions { - if c.SuggestionsMinimumDistance <= 0 { - c.SuggestionsMinimumDistance = 2 - } - if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 { - suggestionsString += "\n\nDid you mean this?\n" - for _, s := range suggestions { - suggestionsString += fmt.Sprintf("\t%v\n", s) - } - } - } - return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString) + if commandFound.TakesArgs == None && len(argsWOflags) > 0 { + return commandFound, a, fmt.Errorf("unknown command %q for %q", argsWOflags[0], commandFound.CommandPath()) } + if commandFound.TakesArgs == ValidOnly && len(commandFound.ValidArgs) > 0 { + for _, v := range argsWOflags { + if !stringInSlice(v, commandFound.ValidArgs) { + return commandFound, a, fmt.Errorf("invalid argument %q for %q%s", v, commandFound.CommandPath(), c.findSuggestions(argsWOflags)) + } + } + } return commandFound, a, nil } +func (c *Command) findSuggestions(argsWOflags []string) string { + if c.DisableSuggestions { + return "" + } + if c.SuggestionsMinimumDistance <= 0 { + c.SuggestionsMinimumDistance = 2 + } + suggestionsString := "" + if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 { + suggestionsString += "\n\nDid you mean this?\n" + for _, s := range suggestions { + suggestionsString += fmt.Sprintf("\t%v\n", s) + } + } + return suggestionsString +} + // SuggestionsFor provides suggestions for the typedName. func (c *Command) SuggestionsFor(typedName string) []string { suggestions := []string{}