Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for case-insensitive command names #1802

Merged
merged 1 commit into from Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 11 additions & 2 deletions cobra.go
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
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
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hooray tests! 👏🏼

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 != "" {
marckhouzam marked this conversation as resolved.
Show resolved Hide resolved
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