Support for case-insensitive command names (#1802)
Add a global `EnableCaseInsensitive` variable to allow case-insensitive command names. The variable supports commands names and aliases globally. Resolves #1382
This commit is contained in:
		
							
								
								
									
										13
									
								
								cobra.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cobra.go
									
									
									
									
									
								
							@ -40,14 +40,23 @@ var templateFuncs = template.FuncMap{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var initializers []func()
 | 
					var initializers []func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						defaultPrefixMatching  = false
 | 
				
			||||||
 | 
						defaultCommandSorting  = true
 | 
				
			||||||
 | 
						defaultCaseInsensitive = false
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// EnablePrefixMatching allows to set automatic prefix matching. Automatic prefix matching can be a dangerous thing
 | 
					// EnablePrefixMatching allows to set automatic prefix matching. Automatic prefix matching can be a dangerous thing
 | 
				
			||||||
// to automatically enable in CLI tools.
 | 
					// to automatically enable in CLI tools.
 | 
				
			||||||
// Set this to true to enable it.
 | 
					// Set this to true to enable it.
 | 
				
			||||||
var EnablePrefixMatching = false
 | 
					var EnablePrefixMatching = defaultPrefixMatching
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// EnableCommandSorting controls sorting of the slice of commands, which is turned on by default.
 | 
					// EnableCommandSorting controls sorting of the slice of commands, which is turned on by default.
 | 
				
			||||||
// To disable sorting, set it to false.
 | 
					// To disable sorting, set it to false.
 | 
				
			||||||
var EnableCommandSorting = true
 | 
					var EnableCommandSorting = defaultCommandSorting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default)
 | 
				
			||||||
 | 
					var EnableCaseInsensitive = defaultCaseInsensitive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MousetrapHelpText enables an information splash screen on Windows
 | 
					// MousetrapHelpText enables an information splash screen on Windows
 | 
				
			||||||
// if the CLI is started from explorer.exe.
 | 
					// if the CLI is started from explorer.exe.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								command.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								command.go
									
									
									
									
									
								
							@ -676,7 +676,7 @@ func (c *Command) findSuggestions(arg string) string {
 | 
				
			|||||||
func (c *Command) findNext(next string) *Command {
 | 
					func (c *Command) findNext(next string) *Command {
 | 
				
			||||||
	matches := make([]*Command, 0)
 | 
						matches := make([]*Command, 0)
 | 
				
			||||||
	for _, cmd := range c.commands {
 | 
						for _, cmd := range c.commands {
 | 
				
			||||||
		if cmd.Name() == next || cmd.HasAlias(next) {
 | 
							if commandNameMatches(cmd.Name(), next) || cmd.HasAlias(next) {
 | 
				
			||||||
			cmd.commandCalledAs.name = next
 | 
								cmd.commandCalledAs.name = next
 | 
				
			||||||
			return cmd
 | 
								return cmd
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -1328,7 +1328,7 @@ func (c *Command) Name() string {
 | 
				
			|||||||
// HasAlias determines if a given string is an alias of the command.
 | 
					// HasAlias determines if a given string is an alias of the command.
 | 
				
			||||||
func (c *Command) HasAlias(s string) bool {
 | 
					func (c *Command) HasAlias(s string) bool {
 | 
				
			||||||
	for _, a := range c.Aliases {
 | 
						for _, a := range c.Aliases {
 | 
				
			||||||
		if a == s {
 | 
							if commandNameMatches(a, s) {
 | 
				
			||||||
			return true
 | 
								return true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -1695,3 +1695,14 @@ func (c *Command) updateParentsPflags() {
 | 
				
			|||||||
		c.parentsPflags.AddFlagSet(parent.PersistentFlags())
 | 
							c.parentsPflags.AddFlagSet(parent.PersistentFlags())
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// commandNameMatches checks if two command names are equal
 | 
				
			||||||
 | 
					// taking into account case sensitivity according to
 | 
				
			||||||
 | 
					// EnableCaseInsensitive global configuration.
 | 
				
			||||||
 | 
					func commandNameMatches(s string, t string) bool {
 | 
				
			||||||
 | 
						if EnableCaseInsensitive {
 | 
				
			||||||
 | 
							return strings.EqualFold(s, t)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return s == t
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										115
									
								
								command_test.go
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								command_test.go
									
									
									
									
									
								
							@ -314,7 +314,7 @@ func TestEnablePrefixMatching(t *testing.T) {
 | 
				
			|||||||
		t.Errorf("aCmdArgs expected: %q, got: %q", onetwo, got)
 | 
							t.Errorf("aCmdArgs expected: %q, got: %q", onetwo, got)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnablePrefixMatching = false
 | 
						EnablePrefixMatching = defaultPrefixMatching
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestAliasPrefixMatching(t *testing.T) {
 | 
					func TestAliasPrefixMatching(t *testing.T) {
 | 
				
			||||||
@ -349,7 +349,7 @@ func TestAliasPrefixMatching(t *testing.T) {
 | 
				
			|||||||
		t.Errorf("timesCmdArgs expected: %v, got: %v", onetwo, got)
 | 
							t.Errorf("timesCmdArgs expected: %v, got: %v", onetwo, got)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnablePrefixMatching = false
 | 
						EnablePrefixMatching = defaultPrefixMatching
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TestChildSameName checks the correct behaviour of cobra in cases,
 | 
					// TestChildSameName checks the correct behaviour of cobra in cases,
 | 
				
			||||||
@ -1263,6 +1263,113 @@ func TestSuggestions(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCaseInsensitive(t *testing.T) {
 | 
				
			||||||
 | 
						rootCmd := &Command{Use: "root", Run: emptyRun}
 | 
				
			||||||
 | 
						childCmd := &Command{Use: "child", Run: emptyRun, Aliases: []string{"alternative"}}
 | 
				
			||||||
 | 
						granchildCmd := &Command{Use: "GRANDCHILD", Run: emptyRun, Aliases: []string{"ALIAS"}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						childCmd.AddCommand(granchildCmd)
 | 
				
			||||||
 | 
						rootCmd.AddCommand(childCmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							args                []string
 | 
				
			||||||
 | 
							failWithoutEnabling bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"child"},
 | 
				
			||||||
 | 
								failWithoutEnabling: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"CHILD"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"chILD"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"CHIld"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"alternative"},
 | 
				
			||||||
 | 
								failWithoutEnabling: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"ALTERNATIVE"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"ALTernatIVE"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"alternatiVE"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"child", "GRANDCHILD"},
 | 
				
			||||||
 | 
								failWithoutEnabling: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"child", "grandchild"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"CHIld", "GRANdchild"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"alternative", "ALIAS"},
 | 
				
			||||||
 | 
								failWithoutEnabling: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"alternative", "alias"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"CHILD", "alias"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								args:                []string{"CHIld", "aliAS"},
 | 
				
			||||||
 | 
								failWithoutEnabling: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							for _, enableCaseInsensitivity := range []bool{true, false} {
 | 
				
			||||||
 | 
								EnableCaseInsensitive = enableCaseInsensitivity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								output, err := executeCommand(rootCmd, test.args...)
 | 
				
			||||||
 | 
								expectedFailure := test.failWithoutEnabling && !enableCaseInsensitivity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !expectedFailure && output != "" {
 | 
				
			||||||
 | 
									t.Errorf("Unexpected output: %v", output)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !expectedFailure && err != nil {
 | 
				
			||||||
 | 
									t.Errorf("Unexpected error: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						EnableCaseInsensitive = defaultCaseInsensitive
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This test make sure we keep backwards-compatibility with respect
 | 
				
			||||||
 | 
					// to command names case sensitivity behavior.
 | 
				
			||||||
 | 
					func TestCaseSensitivityBackwardCompatibility(t *testing.T) {
 | 
				
			||||||
 | 
						rootCmd := &Command{Use: "root", Run: emptyRun}
 | 
				
			||||||
 | 
						childCmd := &Command{Use: "child", Run: emptyRun}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rootCmd.AddCommand(childCmd)
 | 
				
			||||||
 | 
						_, err := executeCommand(rootCmd, strings.ToUpper(childCmd.Use))
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Error("Expected error on calling a command in upper case while command names are case sensitive. Got nil.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRemoveCommand(t *testing.T) {
 | 
					func TestRemoveCommand(t *testing.T) {
 | 
				
			||||||
	rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
 | 
						rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
 | 
				
			||||||
	childCmd := &Command{Use: "child", Run: emptyRun}
 | 
						childCmd := &Command{Use: "child", Run: emptyRun}
 | 
				
			||||||
@ -1622,7 +1729,7 @@ func TestCommandsAreSorted(t *testing.T) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnableCommandSorting = true
 | 
						EnableCommandSorting = defaultCommandSorting
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestEnableCommandSortingIsDisabled(t *testing.T) {
 | 
					func TestEnableCommandSortingIsDisabled(t *testing.T) {
 | 
				
			||||||
@ -1643,7 +1750,7 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnableCommandSorting = true
 | 
						EnableCommandSorting = defaultCommandSorting
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestSetOutput(t *testing.T) {
 | 
					func TestSetOutput(t *testing.T) {
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user