Skip to content

Commit 24ada7f

Browse files
authored
Remove the default "completion" cmd if it is alone (#1559)
When a program has no sub-commands, its root command can accept arguments. If we add the default "completion" command to such programs they will now have a sub-command and will no longer accept arguments. What we do instead for this special case, is only add the "completion" command if it is being called, or if it is being completed itself. We want to have the "completion" command for such programs because it will allow the completion of flags and of arguments (if provided by the program). Signed-off-by: Marc Khouzam <[email protected]>
1 parent 680936a commit 24ada7f

File tree

4 files changed

+179
-13
lines changed

4 files changed

+179
-13
lines changed

command.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -1097,12 +1097,6 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
10971097

10981098
// initialize help at the last point to allow for user overriding
10991099
c.InitDefaultHelpCmd()
1100-
// initialize completion at the last point to allow for user overriding
1101-
c.InitDefaultCompletionCmd()
1102-
1103-
// Now that all commands have been created, let's make sure all groups
1104-
// are properly created also
1105-
c.checkCommandGroups()
11061100

11071101
args := c.args
11081102

@@ -1114,9 +1108,16 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
11141108
args = os.Args[1:]
11151109
}
11161110

1117-
// initialize the hidden command to be used for shell completion
1111+
// initialize the __complete command to be used for shell completion
11181112
c.initCompleteCmd(args)
11191113

1114+
// initialize the default completion command
1115+
c.InitDefaultCompletionCmd(args...)
1116+
1117+
// Now that all commands have been created, let's make sure all groups
1118+
// are properly created also
1119+
c.checkCommandGroups()
1120+
11201121
var flags []string
11211122
if c.TraverseChildren {
11221123
cmd, flags, err = c.Traverse(args)

completions.go

+28-2
Original file line numberDiff line numberDiff line change
@@ -727,8 +727,8 @@ func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*p
727727
// 1- the feature has been explicitly disabled by the program,
728728
// 2- c has no subcommands (to avoid creating one),
729729
// 3- c already has a 'completion' command provided by the program.
730-
func (c *Command) InitDefaultCompletionCmd() {
731-
if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() {
730+
func (c *Command) InitDefaultCompletionCmd(args ...string) {
731+
if c.CompletionOptions.DisableDefaultCmd {
732732
return
733733
}
734734

@@ -741,6 +741,16 @@ func (c *Command) InitDefaultCompletionCmd() {
741741

742742
haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions
743743

744+
// Special case to know if there are sub-commands or not.
745+
hasSubCommands := false
746+
for _, cmd := range c.commands {
747+
if cmd.Name() != ShellCompRequestCmd && cmd.Name() != helpCommandName {
748+
// We found a real sub-command (not 'help' or '__complete')
749+
hasSubCommands = true
750+
break
751+
}
752+
}
753+
744754
completionCmd := &Command{
745755
Use: compCmdName,
746756
Short: "Generate the autocompletion script for the specified shell",
@@ -754,6 +764,22 @@ See each sub-command's help for details on how to use the generated script.
754764
}
755765
c.AddCommand(completionCmd)
756766

767+
if !hasSubCommands {
768+
// If the 'completion' command will be the only sub-command,
769+
// we only create it if it is actually being called.
770+
// This avoids breaking programs that would suddenly find themselves with
771+
// a subcommand, which would prevent them from accepting arguments.
772+
// We also create the 'completion' command if the user is triggering
773+
// shell completion for it (prog __complete completion '')
774+
subCmd, cmdArgs, err := c.Find(args)
775+
if err != nil || subCmd.Name() != compCmdName &&
776+
!(subCmd.Name() == ShellCompRequestCmd && len(cmdArgs) > 1 && cmdArgs[0] == compCmdName) {
777+
// The completion command is not being called or being completed so we remove it.
778+
c.RemoveCommand(completionCmd)
779+
return
780+
}
781+
}
782+
757783
out := c.OutOrStdout()
758784
noDesc := c.CompletionOptions.DisableDescriptions
759785
shortDesc := "Generate the autocompletion script for %s"

completions_test.go

+141-3
Original file line numberDiff line numberDiff line change
@@ -2465,7 +2465,7 @@ func TestDefaultCompletionCmd(t *testing.T) {
24652465
Run: emptyRun,
24662466
}
24672467

2468-
// Test that no completion command is created if there are not other sub-commands
2468+
// Test that when there are no sub-commands, the completion command is not created if it is not called directly.
24692469
assertNoErr(t, rootCmd.Execute())
24702470
for _, cmd := range rootCmd.commands {
24712471
if cmd.Name() == compCmdName {
@@ -2474,6 +2474,17 @@ func TestDefaultCompletionCmd(t *testing.T) {
24742474
}
24752475
}
24762476

2477+
// Test that when there are no sub-commands, the completion command is created when it is called directly.
2478+
_, err := executeCommand(rootCmd, compCmdName)
2479+
if err != nil {
2480+
t.Errorf("Unexpected error: %v", err)
2481+
}
2482+
// Reset the arguments
2483+
rootCmd.args = nil
2484+
// Remove completion command for the next test
2485+
removeCompCmd(rootCmd)
2486+
2487+
// Add a sub-command
24772488
subCmd := &Command{
24782489
Use: "sub",
24792490
Run: emptyRun,
@@ -2595,19 +2606,55 @@ func TestDefaultCompletionCmd(t *testing.T) {
25952606

25962607
func TestCompleteCompletion(t *testing.T) {
25972608
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
2609+
2610+
// Test that when there are no sub-commands, the 'completion' command is not completed
2611+
// (because it is not created).
2612+
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion")
2613+
if err != nil {
2614+
t.Errorf("Unexpected error: %v", err)
2615+
}
2616+
2617+
expected := strings.Join([]string{
2618+
":0",
2619+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
2620+
2621+
if output != expected {
2622+
t.Errorf("expected: %q, got: %q", expected, output)
2623+
}
2624+
2625+
// Test that when there are no sub-commands, completion can be triggered for the default
2626+
// 'completion' command
2627+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion", "")
2628+
if err != nil {
2629+
t.Errorf("Unexpected error: %v", err)
2630+
}
2631+
2632+
expected = strings.Join([]string{
2633+
"bash",
2634+
"fish",
2635+
"powershell",
2636+
"zsh",
2637+
":4",
2638+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2639+
2640+
if output != expected {
2641+
t.Errorf("expected: %q, got: %q", expected, output)
2642+
}
2643+
2644+
// Add a sub-command
25982645
subCmd := &Command{
25992646
Use: "sub",
26002647
Run: emptyRun,
26012648
}
26022649
rootCmd.AddCommand(subCmd)
26032650

26042651
// Test sub-commands of the completion command
2605-
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion", "")
2652+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion", "")
26062653
if err != nil {
26072654
t.Errorf("Unexpected error: %v", err)
26082655
}
26092656

2610-
expected := strings.Join([]string{
2657+
expected = strings.Join([]string{
26112658
"bash",
26122659
"fish",
26132660
"powershell",
@@ -3792,3 +3839,94 @@ func TestDisableDescriptions(t *testing.T) {
37923839
})
37933840
}
37943841
}
3842+
3843+
// A test to make sure the InitDefaultCompletionCmd function works as expected
3844+
// in case a project calls it directly.
3845+
func TestInitDefaultCompletionCmd(t *testing.T) {
3846+
3847+
testCases := []struct {
3848+
desc string
3849+
hasChildCmd bool
3850+
args []string
3851+
expectCompCmd bool
3852+
}{
3853+
{
3854+
desc: "no child command and not calling the completion command",
3855+
hasChildCmd: false,
3856+
args: []string{"somearg"},
3857+
expectCompCmd: false,
3858+
},
3859+
{
3860+
desc: "no child command but calling the completion command",
3861+
hasChildCmd: false,
3862+
args: []string{"completion"},
3863+
expectCompCmd: true,
3864+
},
3865+
{
3866+
desc: "no child command but calling __complete on the root command",
3867+
hasChildCmd: false,
3868+
args: []string{"__complete", ""},
3869+
expectCompCmd: false,
3870+
},
3871+
{
3872+
desc: "no child command but calling __complete on the completion command",
3873+
hasChildCmd: false,
3874+
args: []string{"__complete", "completion", ""},
3875+
expectCompCmd: true,
3876+
},
3877+
{
3878+
desc: "with child command",
3879+
hasChildCmd: true,
3880+
args: []string{"child"},
3881+
expectCompCmd: true,
3882+
},
3883+
{
3884+
desc: "no child command not passing args",
3885+
hasChildCmd: false,
3886+
args: nil,
3887+
expectCompCmd: false,
3888+
},
3889+
{
3890+
desc: "with child command not passing args",
3891+
hasChildCmd: true,
3892+
args: nil,
3893+
expectCompCmd: true,
3894+
},
3895+
}
3896+
3897+
for _, tc := range testCases {
3898+
t.Run(tc.desc, func(t *testing.T) {
3899+
rootCmd := &Command{Use: "root", Run: emptyRun}
3900+
childCmd := &Command{Use: "child", Run: emptyRun}
3901+
3902+
expectedNumSubCommands := 0
3903+
if tc.hasChildCmd {
3904+
rootCmd.AddCommand(childCmd)
3905+
expectedNumSubCommands++
3906+
}
3907+
3908+
if tc.expectCompCmd {
3909+
expectedNumSubCommands++
3910+
}
3911+
3912+
if len(tc.args) > 0 && tc.args[0] == "__complete" {
3913+
expectedNumSubCommands++
3914+
}
3915+
3916+
// Setup the __complete command to mimic real world scenarios
3917+
rootCmd.initCompleteCmd(tc.args)
3918+
3919+
// Call the InitDefaultCompletionCmd function directly
3920+
if tc.args == nil {
3921+
rootCmd.InitDefaultCompletionCmd()
3922+
} else {
3923+
rootCmd.InitDefaultCompletionCmd(tc.args...)
3924+
}
3925+
3926+
// Check if the completion command was added
3927+
if len(rootCmd.Commands()) != expectedNumSubCommands {
3928+
t.Errorf("Expected %d subcommands, got %d", expectedNumSubCommands, len(rootCmd.Commands()))
3929+
}
3930+
})
3931+
}
3932+
}

site/content/completions/_index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ The currently supported shells are:
88
- PowerShell
99

1010
Cobra will automatically provide your program with a fully functional `completion` command,
11-
similarly to how it provides the `help` command.
11+
similarly to how it provides the `help` command. If there are no other subcommands, the
12+
default `completion` command will be hidden, but still functional.
1213

1314
## Creating your own completion command
1415

0 commit comments

Comments
 (0)