Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support validation of positional arguments #284

Merged
merged 2 commits into from
Jul 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,36 @@ 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")
```

## Positional and Custom Arguments

Validation of positional arguments can be specified using the `Args` field.

The follow validators are built in:

- `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 positional 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.
- `ExactArgs(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.

A custom validator can be provided like this:

```go

Args: func validColorArgs(cmd *cobra.Command, args []string) error {
if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
return err
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("Invalid color specified: %s", args[0])
}

```

### Bind Flags with Config

You can also bind your flags with [viper](https:/spf13/viper):
Expand All @@ -480,6 +510,7 @@ when the `--author` flag is not provided by user.

More in [viper documentation](https:/spf13/viper#working-with-flags).


## Example

In the example below, we have defined three commands. Two are at the top level
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a noarg command have subcommands? This seems like the trigger I wanted in #285, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, subcommand lookup happens before this is called in Find, so these arg handling functions only take care of non-subcommand args. I believe this NoArgs option is what you want in #285.

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
}
}

// ExactArgs returns an error if there are not exactly n args
func ExactArgs(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
}
}
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
75 changes: 73 additions & 2 deletions cobra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var cmdHidden = &Command{

var cmdPrint = &Command{
Use: "print [string to print]",
Args: MinimumNArgs(1),
Short: "Print anything to the screen",
Long: `an absolutely utterly useless command for testing.`,
Run: func(cmd *Command, args []string) {
Expand Down Expand Up @@ -75,6 +76,7 @@ var cmdDeprecated = &Command{
Deprecated: "Please use echo instead",
Run: func(cmd *Command, args []string) {
},
Args: NoArgs,
}

var cmdTimes = &Command{
Expand All @@ -88,6 +90,8 @@ var cmdTimes = &Command{
Run: func(cmd *Command, args []string) {
tt = args
},
Args: OnlyValidArgs,
ValidArgs: []string{"one", "two", "three", "four"},
}

var cmdRootNoRun = &Command{
Expand All @@ -105,6 +109,16 @@ var cmdRootSameName = &Command{
Long: "The root description for help",
}

var cmdRootTakesArgs = &Command{
Use: "root-with-args [random args]",
Short: "The root can run it's own function and takes args!",
Copy link

@kofalt kofalt Feb 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/it's/its

Long: "The root description for help, and some args",
Run: func(cmd *Command, args []string) {
tr = args
},
Args: ArbitraryArgs,
}

var cmdRootWithRun = &Command{
Use: "cobra-test",
Short: "The root can run its own function",
Expand Down Expand Up @@ -458,6 +472,63 @@ 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")

if result.Error == nil {
t.Fatal("Expected an error")
}

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")

if result.Error == nil {
t.Fatal("Expected an error")
}

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")

if result.Error == nil {
t.Fatal("Expected an error")
}

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 +743,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
51 changes: 30 additions & 21 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type Command struct {
// but accepted if entered manually.
ArgAliases []string

// Expected arguments
Args PositionalArgs
// BashCompletionFunction is custom functions used by the bash autocompletion generator.
BashCompletionFunction string

Expand Down Expand Up @@ -513,31 +515,27 @@ 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() {
return commandFound, a, nil
if commandFound.Args == nil {
return commandFound, a, legacyArgs(commandFound, stripFlags(a, commandFound))
}
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)
}
}
func (c *Command) findSuggestions(arg string) string {
if c.DisableSuggestions {
return ""
}
if c.SuggestionsMinimumDistance <= 0 {
c.SuggestionsMinimumDistance = 2
}
suggestionsString := ""
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)
}
return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString)
}

return commandFound, a, nil
return suggestionsString
}

// SuggestionsFor provides suggestions for the typedName.
Expand Down Expand Up @@ -624,6 +622,10 @@ func (c *Command) execute(a []string) (err error) {
argWoFlags = a
}

if err := c.ValidateArgs(argWoFlags); err != nil {
return err
}

for p := c; p != nil; p = p.Parent() {
if p.PersistentPreRunE != nil {
if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
Expand Down Expand Up @@ -747,6 +749,13 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
return cmd, err
}

func (c *Command) ValidateArgs(args []string) error {
if c.Args == nil {
return nil
}
return c.Args(c, args)
}

// InitDefaultHelpFlag adds default help flag to c.
// It is called automatically by executing the c or by calling help and usage.
// If c already has help flag, it will do nothing.
Expand Down