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") | ||||
| ``` | ||||
|  | ||||
| ### 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): | ||||
|  | ||||
| @ -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")`) | ||||
|  | ||||
| @ -75,6 +75,7 @@ var cmdDeprecated = &Command{ | ||||
| 	Deprecated: "Please use echo instead", | ||||
| 	Run: func(cmd *Command, args []string) { | ||||
| 	}, | ||||
| 	TakesArgs: None, | ||||
| } | ||||
|  | ||||
| var cmdTimes = &Command{ | ||||
| @ -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{ | ||||
| @ -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{ | ||||
| @ -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") | ||||
|  | ||||
| @ -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) | ||||
| 	} | ||||
|  | ||||
|  | ||||
							
								
								
									
										76
									
								
								command.go
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								command.go
									
									
									
									
									
								
							| @ -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 | ||||
| @ -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 | ||||
|  | ||||
| @ -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) { | ||||
| @ -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) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString) | ||||
| 	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 { | ||||
| 		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{} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user