Skip to content

Commit

Permalink
Allow commands to explicitly state if they do, or do not take arbitra…
Browse files Browse the repository at this point in the history
…ry 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.
  • Loading branch information
eparis authored and n10v committed Jul 23, 2017
1 parent 715f41b commit d89c499
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 21 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions bash_completions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")`)
Expand Down
69 changes: 64 additions & 5 deletions cobra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var cmdDeprecated = &Command{
Deprecated: "Please use echo instead",
Run: func(cmd *Command, args []string) {
},
TakesArgs: None,
}

var cmdTimes = &Command{
Expand All @@ -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{
Expand All @@ -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{
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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)
}

Expand Down
74 changes: 58 additions & 16 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
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, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString)
}

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{}
Expand Down

0 comments on commit d89c499

Please sign in to comment.