Skip to content

Commit

Permalink
Support for case-insensitive command names
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 spf13#1382
  • Loading branch information
YuviGold committed Sep 8, 2022
1 parent 2e8ba6f commit 1a47cd5
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 8 deletions.
21 changes: 19 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 be default)
var EnableCaseInsensitive = DefaultCaseInsensitive

// MousetrapHelpText enables an information splash screen on Windows
// if the CLI is started from explorer.exe.
Expand Down Expand Up @@ -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
}
4 changes: 2 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 IsMatch(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 IsMatch(a, s) {
return true
}
}
Expand Down
101 changes: 97 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,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}
Expand Down Expand Up @@ -1622,7 +1715,7 @@ func TestCommandsAreSorted(t *testing.T) {
}
}

EnableCommandSorting = true
EnableCommandSorting = DefaultCommandSorting
}

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

EnableCommandSorting = true
EnableCommandSorting = DefaultCommandSorting
}

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

0 comments on commit 1a47cd5

Please sign in to comment.