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()
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	defaultPrefixMatching  = false
 | 
			
		||||
	defaultCommandSorting  = true
 | 
			
		||||
	defaultCaseInsensitive = false
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EnablePrefixMatching allows to set automatic prefix matching. Automatic prefix matching can be a dangerous thing
 | 
			
		||||
// to automatically enable in CLI tools.
 | 
			
		||||
// 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.
 | 
			
		||||
// 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
 | 
			
		||||
// 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 {
 | 
			
		||||
	matches := make([]*Command, 0)
 | 
			
		||||
	for _, cmd := range c.commands {
 | 
			
		||||
		if cmd.Name() == next || cmd.HasAlias(next) {
 | 
			
		||||
		if commandNameMatches(cmd.Name(), next) || cmd.HasAlias(next) {
 | 
			
		||||
			cmd.commandCalledAs.name = next
 | 
			
		||||
			return cmd
 | 
			
		||||
		}
 | 
			
		||||
@ -1328,7 +1328,7 @@ func (c *Command) Name() string {
 | 
			
		||||
// HasAlias determines if a given string is an alias of the command.
 | 
			
		||||
func (c *Command) HasAlias(s string) bool {
 | 
			
		||||
	for _, a := range c.Aliases {
 | 
			
		||||
		if a == s {
 | 
			
		||||
		if commandNameMatches(a, s) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -1695,3 +1695,14 @@ func (c *Command) updateParentsPflags() {
 | 
			
		||||
		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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	EnablePrefixMatching = false
 | 
			
		||||
	EnablePrefixMatching = defaultPrefixMatching
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAliasPrefixMatching(t *testing.T) {
 | 
			
		||||
@ -349,7 +349,7 @@ func TestAliasPrefixMatching(t *testing.T) {
 | 
			
		||||
		t.Errorf("timesCmdArgs expected: %v, got: %v", onetwo, got)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	EnablePrefixMatching = false
 | 
			
		||||
	EnablePrefixMatching = defaultPrefixMatching
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	rootCmd := &Command{Use: "root", Args: NoArgs, 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) {
 | 
			
		||||
@ -1643,7 +1750,7 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	EnableCommandSorting = true
 | 
			
		||||
	EnableCommandSorting = defaultCommandSorting
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSetOutput(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user