mirror of
https://github.com/spf13/cobra
synced 2024-11-24 22:57:12 +00:00
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.
This commit is contained in:
parent
715f41bd7a
commit
d89c499964
4 changed files with 157 additions and 22 deletions
32
README.md
32
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")
|
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
|
### Bind Flags with Config
|
||||||
|
|
||||||
You can also bind your flags with [viper](https://github.com/spf13/viper):
|
You can also bind your flags with [viper](https://github.com/spf13/viper):
|
||||||
|
|
|
@ -117,6 +117,8 @@ func TestBashCompletions(t *testing.T) {
|
||||||
// check for filename extension flags
|
// check for filename extension flags
|
||||||
check(t, str, `flags_completion+=("_filedir")`)
|
check(t, str, `flags_completion+=("_filedir")`)
|
||||||
// check for filename extension flags
|
// 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(t, str, `flags_completion+=("__handle_filename_extension_flag json|yaml|yml")`)
|
||||||
// check for custom flags
|
// check for custom flags
|
||||||
check(t, str, `flags_completion+=("__complete_custom")`)
|
check(t, str, `flags_completion+=("__complete_custom")`)
|
||||||
|
|
|
@ -75,6 +75,7 @@ var cmdDeprecated = &Command{
|
||||||
Deprecated: "Please use echo instead",
|
Deprecated: "Please use echo instead",
|
||||||
Run: func(cmd *Command, args []string) {
|
Run: func(cmd *Command, args []string) {
|
||||||
},
|
},
|
||||||
|
TakesArgs: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdTimes = &Command{
|
var cmdTimes = &Command{
|
||||||
|
@ -88,6 +89,8 @@ var cmdTimes = &Command{
|
||||||
Run: func(cmd *Command, args []string) {
|
Run: func(cmd *Command, args []string) {
|
||||||
tt = args
|
tt = args
|
||||||
},
|
},
|
||||||
|
TakesArgs: ValidOnly,
|
||||||
|
ValidArgs: []string{"one", "two", "three", "four"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdRootNoRun = &Command{
|
var cmdRootNoRun = &Command{
|
||||||
|
@ -103,6 +106,17 @@ var cmdRootSameName = &Command{
|
||||||
Use: "print",
|
Use: "print",
|
||||||
Short: "Root with the same name as a subcommand",
|
Short: "Root with the same name as a subcommand",
|
||||||
Long: "The root description for help",
|
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{
|
var cmdRootWithRun = &Command{
|
||||||
|
@ -458,6 +472,51 @@ func TestUsage(t *testing.T) {
|
||||||
checkResultOmits(t, x, cmdCustomFlags.Use+" [flags]")
|
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) {
|
func TestFlagLong(t *testing.T) {
|
||||||
noRRSetupTest("echo", "--intone=13", "something", "--", "here")
|
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
|
// 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)
|
t.Errorf("flags didn't leave proper args remaining. %s given", tt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
58
command.go
58
command.go
|
@ -27,6 +27,15 @@ import (
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Args int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Legacy Args = iota
|
||||||
|
Arbitrary
|
||||||
|
ValidOnly
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
// Command is just that, a command for your application.
|
// Command is just that, a command for your application.
|
||||||
// E.g. 'go run ...' - 'run' is the command. Cobra requires
|
// E.g. 'go run ...' - 'run' is the command. Cobra requires
|
||||||
// you to define the usage and description as part of your command
|
// you to define the usage and description as part of your command
|
||||||
|
@ -59,6 +68,8 @@ type Command struct {
|
||||||
// but accepted if entered manually.
|
// but accepted if entered manually.
|
||||||
ArgAliases []string
|
ArgAliases []string
|
||||||
|
|
||||||
|
// Does this command take arbitrary arguments
|
||||||
|
TakesArgs Args
|
||||||
// BashCompletionFunction is custom functions used by the bash autocompletion generator.
|
// BashCompletionFunction is custom functions used by the bash autocompletion generator.
|
||||||
BashCompletionFunction string
|
BashCompletionFunction string
|
||||||
|
|
||||||
|
@ -472,6 +483,15 @@ func argsMinusFirstX(args []string, x string) []string {
|
||||||
return args
|
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
|
// Find the target command given the args and command tree
|
||||||
// Meant to be run on the highest node. Only searches down.
|
// Meant to be run on the highest node. Only searches down.
|
||||||
func (c *Command) Find(args []string) (*Command, []string, error) {
|
func (c *Command) Find(args []string) (*Command, []string, error) {
|
||||||
|
@ -515,29 +535,51 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
|
||||||
commandFound, a := innerfind(c, args)
|
commandFound, a := innerfind(c, args)
|
||||||
argsWOflags := stripFlags(a, commandFound)
|
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
|
// no subcommand, always take args
|
||||||
if !commandFound.HasSubCommands() {
|
if !commandFound.HasSubCommands() {
|
||||||
return commandFound, a, nil
|
return commandFound, a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// root command with subcommands, do subcommand checking
|
// root command with subcommands, do subcommand checking
|
||||||
if commandFound == c && len(argsWOflags) > 0 {
|
if commandFound == c && len(argsWOflags) > 0 {
|
||||||
suggestionsString := ""
|
return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), c.findSuggestions(argsWOflags))
|
||||||
if !c.DisableSuggestions {
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commandFound, a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) findSuggestions(argsWOflags []string) string {
|
||||||
|
if c.DisableSuggestions {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
if c.SuggestionsMinimumDistance <= 0 {
|
if c.SuggestionsMinimumDistance <= 0 {
|
||||||
c.SuggestionsMinimumDistance = 2
|
c.SuggestionsMinimumDistance = 2
|
||||||
}
|
}
|
||||||
|
suggestionsString := ""
|
||||||
if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 {
|
if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 {
|
||||||
suggestionsString += "\n\nDid you mean this?\n"
|
suggestionsString += "\n\nDid you mean this?\n"
|
||||||
for _, s := range suggestions {
|
for _, s := range suggestions {
|
||||||
suggestionsString += fmt.Sprintf("\t%v\n", s)
|
suggestionsString += fmt.Sprintf("\t%v\n", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return suggestionsString
|
||||||
return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString)
|
|
||||||
}
|
|
||||||
|
|
||||||
return commandFound, a, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuggestionsFor provides suggestions for the typedName.
|
// SuggestionsFor provides suggestions for the typedName.
|
||||||
|
|
Loading…
Reference in a new issue