- adds support for usage strings on flag errors
- adds support for arg validation
Signed-off-by: Daniel Nephin <dnephin@docker.com>
| ... | ... |
@@ -135,7 +135,7 @@ clone git google.golang.org/cloud dae7e3d993bc3812a2185af60552bb6b847e52a0 https |
| 135 | 135 |
clone git github.com/docker/containerd 57b7c3da915ebe943bd304c00890959b191e5264 |
| 136 | 136 |
|
| 137 | 137 |
# cli |
| 138 |
-clone git github.com/spf13/cobra 0f866a6211e33cde2091d9290c08f6afd6c9ebbc |
|
| 138 |
+clone git github.com/spf13/cobra acf60156558542e78c6f3695f74b0f871614ff55 https://github.com/dnephin/cobra.git |
|
| 139 | 139 |
clone git github.com/spf13/pflag cb88ea77998c3f024757528e3305022ab50b43be |
| 140 | 140 |
clone git github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 |
| 141 | 141 |
|
| ... | ... |
@@ -406,6 +406,42 @@ A flag can also be assigned locally which will only apply to that specific comma |
| 406 | 406 |
RootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from") |
| 407 | 407 |
``` |
| 408 | 408 |
|
| 409 |
+### Positional Arguments |
|
| 410 |
+ |
|
| 411 |
+Validation of positional arguments can be specified using the `Args` field, which accepts |
|
| 412 |
+one of the following values: |
|
| 413 |
+ |
|
| 414 |
+- `NoArgs` - the command will report an error if there are any positional args. |
|
| 415 |
+- `ArbitraryArgs` - the command will accept any args. |
|
| 416 |
+- `OnlyValidArgs` - the command will report an error if there are any positiona |
|
| 417 |
+ args that are not in the `ValidArgs` list. |
|
| 418 |
+- `MinimumNArgs(int)` - the command will report an error if there are not at |
|
| 419 |
+ least N positional args. |
|
| 420 |
+- `MaximumNArgs(int)` - the command will report an error if there are more than |
|
| 421 |
+ N positional args. |
|
| 422 |
+- `ExactArgs(int)` - the command will report an error if there are not |
|
| 423 |
+ exactly N positional args. |
|
| 424 |
+- `RangeArgs(min, max)` - the command will report an error if the number of args |
|
| 425 |
+ is not between the minimum and maximum number of expected args. |
|
| 426 |
+ |
|
| 427 |
+By default, `Args` uses the following legacy behaviour: |
|
| 428 |
+- root commands with no subcommands can take arbitrary arguments |
|
| 429 |
+- root commands with subcommands will do subcommand validity checking |
|
| 430 |
+- subcommands will always accept arbitrary arguments and do no subsubcommand validity checking |
|
| 431 |
+ |
|
| 432 |
+ |
|
| 433 |
+```go |
|
| 434 |
+var HugoCmd = &cobra.Command{
|
|
| 435 |
+ Use: "hugo", |
|
| 436 |
+ Short: "Hugo is a very fast static site generator", |
|
| 437 |
+ ValidArgs: []string{"one", "two"}
|
|
| 438 |
+ Args: cobra.OnlyValidArgs |
|
| 439 |
+ Run: func(cmd *cobra.Command, args []string) {
|
|
| 440 |
+ // args will only have the values one, two |
|
| 441 |
+ // or the cmd.Execute() will fail. |
|
| 442 |
+ }, |
|
| 443 |
+} |
|
| 444 |
+``` |
|
| 409 | 445 |
|
| 410 | 446 |
## Example |
| 411 | 447 |
|
| 412 | 448 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,98 @@ |
| 0 |
+package cobra |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+type PositionalArgs func(cmd *Command, args []string) error |
|
| 7 |
+ |
|
| 8 |
+// Legacy arg validation has the following behaviour: |
|
| 9 |
+// - root commands with no subcommands can take arbitrary arguments |
|
| 10 |
+// - root commands with subcommands will do subcommand validity checking |
|
| 11 |
+// - subcommands will always accept arbitrary arguments |
|
| 12 |
+func legacyArgs(cmd *Command, args []string) error {
|
|
| 13 |
+ // no subcommand, always take args |
|
| 14 |
+ if !cmd.HasSubCommands() {
|
|
| 15 |
+ return nil |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ // root command with subcommands, do subcommand checking |
|
| 19 |
+ if !cmd.HasParent() && len(args) > 0 {
|
|
| 20 |
+ return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
|
|
| 21 |
+ } |
|
| 22 |
+ return nil |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+// NoArgs returns an error if any args are included |
|
| 26 |
+func NoArgs(cmd *Command, args []string) error {
|
|
| 27 |
+ if len(args) > 0 {
|
|
| 28 |
+ return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
|
|
| 29 |
+ } |
|
| 30 |
+ return nil |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+// OnlyValidArgs returns an error if any args are not in the list of ValidArgs |
|
| 34 |
+func OnlyValidArgs(cmd *Command, args []string) error {
|
|
| 35 |
+ if len(cmd.ValidArgs) > 0 {
|
|
| 36 |
+ for _, v := range args {
|
|
| 37 |
+ if !stringInSlice(v, cmd.ValidArgs) {
|
|
| 38 |
+ return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0]))
|
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ } |
|
| 42 |
+ return nil |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+func stringInSlice(a string, list []string) bool {
|
|
| 46 |
+ for _, b := range list {
|
|
| 47 |
+ if b == a {
|
|
| 48 |
+ return true |
|
| 49 |
+ } |
|
| 50 |
+ } |
|
| 51 |
+ return false |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+// ArbitraryArgs never returns an error |
|
| 55 |
+func ArbitraryArgs(cmd *Command, args []string) error {
|
|
| 56 |
+ return nil |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+// MinimumNArgs returns an error if there is not at least N args |
|
| 60 |
+func MinimumNArgs(n int) PositionalArgs {
|
|
| 61 |
+ return func(cmd *Command, args []string) error {
|
|
| 62 |
+ if len(args) < n {
|
|
| 63 |
+ return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args))
|
|
| 64 |
+ } |
|
| 65 |
+ return nil |
|
| 66 |
+ } |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 69 |
+// MaximumNArgs returns an error if there are more than N args |
|
| 70 |
+func MaximumNArgs(n int) PositionalArgs {
|
|
| 71 |
+ return func(cmd *Command, args []string) error {
|
|
| 72 |
+ if len(args) > n {
|
|
| 73 |
+ return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args))
|
|
| 74 |
+ } |
|
| 75 |
+ return nil |
|
| 76 |
+ } |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+// ExactArgs returns an error if there are not exactly n args |
|
| 80 |
+func ExactArgs(n int) PositionalArgs {
|
|
| 81 |
+ return func(cmd *Command, args []string) error {
|
|
| 82 |
+ if len(args) != n {
|
|
| 83 |
+ return fmt.Errorf("accepts %d arg(s), received %d", n, len(args))
|
|
| 84 |
+ } |
|
| 85 |
+ return nil |
|
| 86 |
+ } |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 89 |
+// RangeArgs returns an error if the number of args is not within the expected range |
|
| 90 |
+func RangeArgs(min int, max int) PositionalArgs {
|
|
| 91 |
+ return func(cmd *Command, args []string) error {
|
|
| 92 |
+ if len(args) < min || len(args) > max {
|
|
| 93 |
+ return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args))
|
|
| 94 |
+ } |
|
| 95 |
+ return nil |
|
| 96 |
+ } |
|
| 97 |
+} |
| ... | ... |
@@ -50,6 +50,8 @@ type Command struct {
|
| 50 | 50 |
// List of aliases for ValidArgs. These are not suggested to the user in the bash |
| 51 | 51 |
// completion, but accepted if entered manually. |
| 52 | 52 |
ArgAliases []string |
| 53 |
+ // Expected arguments |
|
| 54 |
+ Args PositionalArgs |
|
| 53 | 55 |
// Custom functions used by the bash autocompletion generator |
| 54 | 56 |
BashCompletionFunction string |
| 55 | 57 |
// Is this command deprecated and should print this string when used? |
| ... | ... |
@@ -110,6 +112,7 @@ type Command struct {
|
| 110 | 110 |
output *io.Writer // nil means stderr; use Out() method instead |
| 111 | 111 |
usageFunc func(*Command) error // Usage can be defined by application |
| 112 | 112 |
usageTemplate string // Can be defined by Application |
| 113 |
+ flagErrorFunc func(*Command, error) error |
|
| 113 | 114 |
helpTemplate string // Can be defined by Application |
| 114 | 115 |
helpFunc func(*Command, []string) // Help can be defined by application |
| 115 | 116 |
helpCommand *Command // The help command |
| ... | ... |
@@ -163,6 +166,12 @@ func (c *Command) SetUsageTemplate(s string) {
|
| 163 | 163 |
c.usageTemplate = s |
| 164 | 164 |
} |
| 165 | 165 |
|
| 166 |
+// SetFlagErrorFunc sets a function to generate an error when flag parsing |
|
| 167 |
+// fails |
|
| 168 |
+func (c *Command) SetFlagErrorFunc(f func(*Command, error) error) {
|
|
| 169 |
+ c.flagErrorFunc = f |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 166 | 172 |
// Can be defined by Application |
| 167 | 173 |
func (c *Command) SetHelpFunc(f func(*Command, []string)) {
|
| 168 | 174 |
c.helpFunc = f |
| ... | ... |
@@ -224,6 +233,22 @@ func (c *Command) HelpFunc() func(*Command, []string) {
|
| 224 | 224 |
} |
| 225 | 225 |
} |
| 226 | 226 |
|
| 227 |
+// FlagErrorFunc returns either the function set by SetFlagErrorFunc for this |
|
| 228 |
+// command or a parent, or it returns a function which returns the original |
|
| 229 |
+// error. |
|
| 230 |
+func (c *Command) FlagErrorFunc() (f func(*Command, error) error) {
|
|
| 231 |
+ if c.flagErrorFunc != nil {
|
|
| 232 |
+ return c.flagErrorFunc |
|
| 233 |
+ } |
|
| 234 |
+ |
|
| 235 |
+ if c.HasParent() {
|
|
| 236 |
+ return c.parent.FlagErrorFunc() |
|
| 237 |
+ } |
|
| 238 |
+ return func(c *Command, err error) error {
|
|
| 239 |
+ return err |
|
| 240 |
+ } |
|
| 241 |
+} |
|
| 242 |
+ |
|
| 227 | 243 |
var minUsagePadding = 25 |
| 228 | 244 |
|
| 229 | 245 |
func (c *Command) UsagePadding() int {
|
| ... | ... |
@@ -422,31 +447,27 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
|
| 422 | 422 |
} |
| 423 | 423 |
|
| 424 | 424 |
commandFound, a := innerfind(c, args) |
| 425 |
- argsWOflags := stripFlags(a, commandFound) |
|
| 426 |
- |
|
| 427 |
- // no subcommand, always take args |
|
| 428 |
- if !commandFound.HasSubCommands() {
|
|
| 429 |
- return commandFound, a, nil |
|
| 425 |
+ if commandFound.Args == nil {
|
|
| 426 |
+ return commandFound, a, legacyArgs(commandFound, stripFlags(a, commandFound)) |
|
| 430 | 427 |
} |
| 428 |
+ return commandFound, a, nil |
|
| 429 |
+} |
|
| 431 | 430 |
|
| 432 |
- // root command with subcommands, do subcommand checking |
|
| 433 |
- if commandFound == c && len(argsWOflags) > 0 {
|
|
| 434 |
- suggestionsString := "" |
|
| 435 |
- if !c.DisableSuggestions {
|
|
| 436 |
- if c.SuggestionsMinimumDistance <= 0 {
|
|
| 437 |
- c.SuggestionsMinimumDistance = 2 |
|
| 438 |
- } |
|
| 439 |
- if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 {
|
|
| 440 |
- suggestionsString += "\n\nDid you mean this?\n" |
|
| 441 |
- for _, s := range suggestions {
|
|
| 442 |
- suggestionsString += fmt.Sprintf("\t%v\n", s)
|
|
| 443 |
- } |
|
| 444 |
- } |
|
| 431 |
+func (c *Command) findSuggestions(arg string) string {
|
|
| 432 |
+ if c.DisableSuggestions {
|
|
| 433 |
+ return "" |
|
| 434 |
+ } |
|
| 435 |
+ if c.SuggestionsMinimumDistance <= 0 {
|
|
| 436 |
+ c.SuggestionsMinimumDistance = 2 |
|
| 437 |
+ } |
|
| 438 |
+ suggestionsString := "" |
|
| 439 |
+ if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 {
|
|
| 440 |
+ suggestionsString += "\n\nDid you mean this?\n" |
|
| 441 |
+ for _, s := range suggestions {
|
|
| 442 |
+ suggestionsString += fmt.Sprintf("\t%v\n", s)
|
|
| 445 | 443 |
} |
| 446 |
- return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString)
|
|
| 447 | 444 |
} |
| 448 |
- |
|
| 449 |
- return commandFound, a, nil |
|
| 445 |
+ return suggestionsString |
|
| 450 | 446 |
} |
| 451 | 447 |
|
| 452 | 448 |
func (c *Command) SuggestionsFor(typedName string) []string {
|
| ... | ... |
@@ -520,7 +541,7 @@ func (c *Command) execute(a []string) (err error) {
|
| 520 | 520 |
|
| 521 | 521 |
err = c.ParseFlags(a) |
| 522 | 522 |
if err != nil {
|
| 523 |
- return err |
|
| 523 |
+ return c.FlagErrorFunc()(c, err) |
|
| 524 | 524 |
} |
| 525 | 525 |
// If help is called, regardless of other flags, return we want help |
| 526 | 526 |
// Also say we need help if the command isn't runnable. |
| ... | ... |
@@ -535,6 +556,10 @@ func (c *Command) execute(a []string) (err error) {
|
| 535 | 535 |
return flag.ErrHelp |
| 536 | 536 |
} |
| 537 | 537 |
|
| 538 |
+ if err := c.ValidateArgs(a); err != nil {
|
|
| 539 |
+ return err |
|
| 540 |
+ } |
|
| 541 |
+ |
|
| 538 | 542 |
c.preRun() |
| 539 | 543 |
argWoFlags := c.Flags().Args() |
| 540 | 544 |
|
| ... | ... |
@@ -673,7 +698,15 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
|
| 673 | 673 |
return cmd, nil |
| 674 | 674 |
} |
| 675 | 675 |
|
| 676 |
+func (c *Command) ValidateArgs(args []string) error {
|
|
| 677 |
+ if c.Args == nil {
|
|
| 678 |
+ return nil |
|
| 679 |
+ } |
|
| 680 |
+ return c.Args(c, stripFlags(args, c)) |
|
| 681 |
+} |
|
| 682 |
+ |
|
| 676 | 683 |
func (c *Command) initHelpFlag() {
|
| 684 |
+ c.mergePersistentFlags() |
|
| 677 | 685 |
if c.Flags().Lookup("help") == nil {
|
| 678 | 686 |
c.Flags().BoolP("help", "h", false, "help for "+c.Name())
|
| 679 | 687 |
} |
| ... | ... |
@@ -747,7 +780,7 @@ func (c *Command) AddCommand(cmds ...*Command) {
|
| 747 | 747 |
} |
| 748 | 748 |
} |
| 749 | 749 |
|
| 750 |
-// AddCommand removes one or more commands from a parent command. |
|
| 750 |
+// RemoveCommand removes one or more commands from a parent command. |
|
| 751 | 751 |
func (c *Command) RemoveCommand(cmds ...*Command) {
|
| 752 | 752 |
commands := []*Command{}
|
| 753 | 753 |
main: |