Skip to content

Commit

Permalink
Support for case-insensitive command names (#1802)
Browse files Browse the repository at this point in the history
Add a global `EnableCaseInsensitive` variable to allow
case-insensitive command names.

The variable supports commands names and aliases globally.

Resolves #1382
  • Loading branch information
YuviGold authored Sep 11, 2022
1 parent 70e53f6 commit d689184
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 8 deletions.
13 changes: 11 additions & 2 deletions cobra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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 changes: 111 additions & 4 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -1622,7 +1729,7 @@ func TestCommandsAreSorted(t *testing.T) {
}
}

EnableCommandSorting = true
EnableCommandSorting = defaultCommandSorting
}

func TestEnableCommandSortingIsDisabled(t *testing.T) {
Expand All @@ -1643,7 +1750,7 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) {
}
}

EnableCommandSorting = true
EnableCommandSorting = defaultCommandSorting
}

func TestSetOutput(t *testing.T) {
Expand Down

0 comments on commit d689184

Please sign in to comment.