From 1a47cd5fed48e833aa697ba1362e78b8923fff17 Mon Sep 17 00:00:00 2001 From: Yuval Goldberg Date: Thu, 8 Sep 2022 18:37:17 +0300 Subject: [PATCH] Support for case-insensitive command names Add a global `EnableCaseInsensitive` variable to allow case-insensitive command names. The variable supports commands names and aliases globally. Resolves #1382 --- cobra.go | 21 +++++++++- command.go | 4 +- command_test.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/cobra.go b/cobra.go index d6cbfd719..b21eeb93f 100644 --- a/cobra.go +++ b/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 be default) +var EnableCaseInsensitive = DefaultCaseInsensitive // MousetrapHelpText enables an information splash screen on Windows // if the CLI is started from explorer.exe. @@ -220,3 +229,11 @@ func WriteStringAndCheck(b io.StringWriter, s string) { _, err := b.WriteString(s) CheckErr(err) } + +func IsMatch(s string, t string) bool { + if EnableCaseInsensitive { + return strings.EqualFold(s, t) + } + + return s == t +} diff --git a/command.go b/command.go index 6e611337e..93e555fd4 100644 --- a/command.go +++ b/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 IsMatch(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 IsMatch(a, s) { return true } } diff --git a/command_test.go b/command_test.go index 0b0b82132..e49851f90 100644 --- a/command_test.go +++ b/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,99 @@ 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 +} + func TestRemoveCommand(t *testing.T) { rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} childCmd := &Command{Use: "child", Run: emptyRun} @@ -1622,7 +1715,7 @@ func TestCommandsAreSorted(t *testing.T) { } } - EnableCommandSorting = true + EnableCommandSorting = DefaultCommandSorting } func TestEnableCommandSortingIsDisabled(t *testing.T) { @@ -1643,7 +1736,7 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) { } } - EnableCommandSorting = true + EnableCommandSorting = DefaultCommandSorting } func TestSetOutput(t *testing.T) {