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:
		
				
					committed by
					
						 Albert Nigmatzianov
						Albert Nigmatzianov
					
				
			
			
				
	
			
			
			
						parent
						
							715f41bd7a
						
					
				
				
					commit
					d89c499964
				
			
							
								
								
									
										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{ | ||||||
| @ -100,9 +103,20 @@ var cmdRootNoRun = &Command{ | |||||||
| } | } | ||||||
|  |  | ||||||
| var cmdRootSameName = &Command{ | 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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										76
									
								
								command.go
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								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,31 +535,53 @@ 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) | ||||||
|  |  | ||||||
| 	// no subcommand, always take args | 	// "Legacy" has some 'odd' characteristics. | ||||||
| 	if !commandFound.HasSubCommands() { | 	// - 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 | 		return commandFound, a, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// root command with subcommands, do subcommand checking | 	if commandFound.TakesArgs == None && len(argsWOflags) > 0 { | ||||||
| 	if commandFound == c && len(argsWOflags) > 0 { | 		return commandFound, a, fmt.Errorf("unknown command %q for %q", argsWOflags[0], commandFound.CommandPath()) | ||||||
| 		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) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	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 | 	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. | // SuggestionsFor provides suggestions for the typedName. | ||||||
| func (c *Command) SuggestionsFor(typedName string) []string { | func (c *Command) SuggestionsFor(typedName string) []string { | ||||||
| 	suggestions := []string{} | 	suggestions := []string{} | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user