Skip to content

Commit

Permalink
Refactor TakesArgs to use an interface for arg validation.
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
  • Loading branch information
dnephin committed May 27, 2016
1 parent 88bd5f7 commit 2d158e1
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 66 deletions.
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
98 changes: 98 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 4 additions & 4 deletions cobra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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"},
}

Expand All @@ -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{
Expand All @@ -120,7 +120,7 @@ var cmdRootTakesArgs = &Command{
Run: func(cmd *Command, args []string) {
tr = args
},
TakesArgs: Arbitrary,
Args: ArbitraryArgs,
}

var cmdRootWithRun = &Command{
Expand Down
54 changes: 6 additions & 48 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -444,45 +426,21 @@ 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 ""
}
if c.SuggestionsMinimumDistance <= 0 {
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)
Expand Down

0 comments on commit 2d158e1

Please sign in to comment.