From 2d158e16d2a66ecda86c4a944faed08ae348e13f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 May 2016 21:46:49 -0700 Subject: [PATCH] Refactor TakesArgs to use an interface for arg validation. Signed-off-by: Daniel Nephin --- README.md | 32 +++++++++-------- args.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ cobra_test.go | 8 ++--- command.go | 54 ++++------------------------ 4 files changed, 126 insertions(+), 66 deletions(-) create mode 100644 args.go diff --git a/README.md b/README.md index 66d0b16910..b17357078f 100644 --- a/README.md +++ b/README.md @@ -406,33 +406,37 @@ 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") ``` -### Expected Arguments +### Positional Arguments -Expected arguments can be specified using the `TakesArgs` field, which accepts +Validation of positional arguments can be specified using the `Args` field, which accepts one of the following values: -- `Legacy` -- `None` -- `Arbitrary` -- `ValidOnly` - -`Legacy` (the default): +- `NoArgs` - the command will report an error if there are any positional args. +- `ArbitraryArgs` - the command will accept any args. +- `OnlyValidArgs` - the command will report an error if there are any positiona + args that are not in the `ValidArgs` list. +- `MinimumNArgs(int)` - the command will report an error if there are not at + least N positional args. +- `MaximumNArgs(int)` - the command will report an error if there are more than + N positional args. +- `ExactlyNArgs(int)` - the command will report an error if there are not + exactly + N positional args. +- `RangeArgs(min, max)` - the command will report an error if the number of args + is not between the minimum and maximum number of expected args. + +By default, `Args` uses the following legacy behaviour: - 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 to the `Run` function. - -`ValidOnly` - all valid (non-subcommand) arguments must be defined in the `ValidArgs` field. 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"} - TakesArgs: cobra.ValidOnly + Args: cobra.OnlyValidArgs Run: func(cmd *cobra.Command, args []string) { // args will only have the values one, two // or the cmd.Execute() will fail. diff --git a/args.go b/args.go new file mode 100644 index 0000000000..1f6f10162a --- /dev/null +++ b/args.go @@ -0,0 +1,98 @@ +package cobra + +import ( + "fmt" +) + +type PositionalArgs func(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 +// - subcommands will always accept arbitrary arguments +func legacyArgs(cmd *Command, args []string) error { + // no subcommand, always take args + if !cmd.HasSubCommands() { + return nil + } + + // 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.findSuggestions(args[0])) + } + return nil +} + +// NoArgs returns an error if any args are included +func NoArgs(cmd *Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + } + return nil +} + +// OnlyValidArgs returns an error if any args are not in the list of ValidArgs +func OnlyValidArgs(cmd *Command, args []string) error { + if len(cmd.ValidArgs) > 0 { + for _, v := range args { + if !stringInSlice(v, cmd.ValidArgs) { + return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0])) + } + } + } + return nil +} + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +// ArbitraryArgs never returns an error +func ArbitraryArgs(cmd *Command, args []string) error { + return nil +} + +// MinimumNArgs returns an error if there is not at least N args +func MinimumNArgs(n int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) < n { + return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args)) + } + return nil + } +} + +// MaximumNArgs returns an error if there are more than N args +func MaximumNArgs(n int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) > n { + return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args)) + } + return nil + } +} + +// ExactlyNArgs returns an error if there are not exactly n args +func ExactlyNArgs(n int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) != n { + return fmt.Errorf("accepts %d arg(s), received %d", n, len(args)) + } + return nil + } +} + +// RangeArgs returns an error if the number of args is not within the expected range +func RangeArgs(min int, max int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) < min || len(args) > max { + return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args)) + } + return nil + } +} diff --git a/cobra_test.go b/cobra_test.go index 4ce4e5f786..cc78ff6712 100644 --- a/cobra_test.go +++ b/cobra_test.go @@ -79,7 +79,7 @@ var cmdDeprecated = &Command{ Deprecated: "Please use echo instead", Run: func(cmd *Command, args []string) { }, - TakesArgs: None, + Args: NoArgs, } var cmdTimes = &Command{ @@ -93,7 +93,7 @@ var cmdTimes = &Command{ Run: func(cmd *Command, args []string) { tt = args }, - TakesArgs: ValidOnly, + Args: OnlyValidArgs, ValidArgs: []string{"one", "two", "three", "four"}, } @@ -110,7 +110,7 @@ var cmdRootSameName = &Command{ Use: "print", Short: "Root with the same name as a subcommand", Long: "The root description for help", - TakesArgs: None, + Args: NoArgs, } var cmdRootTakesArgs = &Command{ @@ -120,7 +120,7 @@ var cmdRootTakesArgs = &Command{ Run: func(cmd *Command, args []string) { tr = args }, - TakesArgs: Arbitrary, + Args: ArbitraryArgs, } var cmdRootWithRun = &Command{ diff --git a/command.go b/command.go index 642e790c21..d10ecd0bfa 100644 --- a/command.go +++ b/command.go @@ -26,15 +26,6 @@ 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. // eg. 'go run' ... 'run' is the command. Cobra requires // you to define the usage and description as part of your command @@ -60,7 +51,7 @@ type Command struct { // completion, but accepted if entered manually. ArgAliases []string // Expected arguments - TakesArgs Args + Args PositionalArgs // Custom functions used by the bash autocompletion generator BashCompletionFunction string // Is this command deprecated and should print this string when used? @@ -392,15 +383,6 @@ 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) { @@ -444,37 +426,13 @@ func (c *Command) Find(args []string) (*Command, []string, error) { commandFound, a := innerfind(c, args) argsWOflags := stripFlags(a, commandFound) - // "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 - } - - 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)) - } - } + if commandFound.Args == nil { + commandFound.Args = legacyArgs } - return commandFound, a, nil + return commandFound, a, commandFound.Args(commandFound, argsWOflags) } -func (c *Command) findSuggestions(argsWOflags []string) string { +func (c *Command) findSuggestions(arg string) string { if c.DisableSuggestions { return "" } @@ -482,7 +440,7 @@ func (c *Command) findSuggestions(argsWOflags []string) string { c.SuggestionsMinimumDistance = 2 } suggestionsString := "" - if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 { + if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 { suggestionsString += "\n\nDid you mean this?\n" for _, s := range suggestions { suggestionsString += fmt.Sprintf("\t%v\n", s)