diff --git a/command.go b/command.go index 01f7c6f1c..a78394e75 100644 --- a/command.go +++ b/command.go @@ -1019,12 +1019,6 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { // initialize help at the last point to allow for user overriding c.InitDefaultHelpCmd() - // initialize completion at the last point to allow for user overriding - c.InitDefaultCompletionCmd() - - // Now that all commands have been created, let's make sure all groups - // are properly created also - c.checkCommandGroups() args := c.args @@ -1033,9 +1027,16 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { args = os.Args[1:] } - // initialize the hidden command to be used for shell completion + // initialize the __complete command to be used for shell completion c.initCompleteCmd(args) + // initialize the default completion command + c.InitDefaultCompletionCmd(args) + + // Now that all commands have been created, let's make sure all groups + // are properly created also + c.checkCommandGroups() + var flags []string if c.TraverseChildren { cmd, flags, err = c.Traverse(args) diff --git a/completions.go b/completions.go index ee38c4d0b..dd77b8d43 100644 --- a/completions.go +++ b/completions.go @@ -657,8 +657,8 @@ func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*p // 1- the feature has been explicitly disabled by the program, // 2- c has no subcommands (to avoid creating one), // 3- c already has a 'completion' command provided by the program. -func (c *Command) InitDefaultCompletionCmd() { - if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() { +func (c *Command) InitDefaultCompletionCmd(args []string) { + if c.CompletionOptions.DisableDefaultCmd { return } @@ -670,6 +670,10 @@ func (c *Command) InitDefaultCompletionCmd() { } haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions + // Special case to know if there are sub-commands or not. + // If there is exactly 1 sub-command, it must be the __complete command, so we are looking for the case + // where there are *more* than one sub-commands: the _complete command *and* a real sub-command. + hasSubCommands := len(c.commands) > 1 completionCmd := &Command{ Use: compCmdName, @@ -684,6 +688,22 @@ See each sub-command's help for details on how to use the generated script. } c.AddCommand(completionCmd) + if !hasSubCommands { + // If the 'completion' command will be the only sub-command (other than '__complete'), + // we only create it if it is actually being called. + // This avoids breaking programs that would suddenly find themselves with + // a subcommand, which would prevent them from accepting arguments. + // We also create the 'completion' command if the user is triggering + // shell completion for it (prog __complete completion '') + subCmd, cmdArgs, err := c.Find(args) + if err != nil || subCmd.Name() != compCmdName && + !(subCmd.Name() == ShellCompRequestCmd && len(cmdArgs) > 1 && cmdArgs[0] == compCmdName) { + // The completion command is not being called or being completed so we remove it. + c.RemoveCommand(completionCmd) + return + } + } + out := c.OutOrStdout() noDesc := c.CompletionOptions.DisableDescriptions shortDesc := "Generate the autocompletion script for %s" diff --git a/completions_test.go b/completions_test.go index 0588da0f1..8d5b94cf7 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2321,7 +2321,7 @@ func TestDefaultCompletionCmd(t *testing.T) { Run: emptyRun, } - // Test that no completion command is created if there are not other sub-commands + // Test that when there are no sub-commands, the completion command is not created if it is not called directly. assertNoErr(t, rootCmd.Execute()) for _, cmd := range rootCmd.commands { if cmd.Name() == compCmdName { @@ -2330,6 +2330,17 @@ func TestDefaultCompletionCmd(t *testing.T) { } } + // Test that when there are no sub-commands, the completion command is created when it is called directly. + _, err := executeCommand(rootCmd, compCmdName) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + // Reset the arguments + rootCmd.args = nil + // Remove completion command for the next test + removeCompCmd(rootCmd) + + // Add a sub-command subCmd := &Command{ Use: "sub", Run: emptyRun, @@ -2451,6 +2462,42 @@ func TestDefaultCompletionCmd(t *testing.T) { func TestCompleteCompletion(t *testing.T) { rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} + + // Test that when there are no sub-commands, the 'completion' command is not completed + // (because it is not created). + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that when there are no sub-commands, completion can be triggered for the default + // 'completion' command + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "bash", + "fish", + "powershell", + "zsh", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Add a sub-command subCmd := &Command{ Use: "sub", Run: emptyRun, @@ -2458,12 +2505,12 @@ func TestCompleteCompletion(t *testing.T) { rootCmd.AddCommand(subCmd) // Test sub-commands of the completion command - output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion", "") + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "completion", "") if err != nil { t.Errorf("Unexpected error: %v", err) } - expected := strings.Join([]string{ + expected = strings.Join([]string{ "bash", "fish", "powershell", diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 4efad2907..70981ddd9 100644 --- a/site/content/completions/_index.md +++ b/site/content/completions/_index.md @@ -8,7 +8,8 @@ The currently supported shells are: - PowerShell Cobra will automatically provide your program with a fully functional `completion` command, -similarly to how it provides the `help` command. +similarly to how it provides the `help` command. If there are no other subcommands, the +default `completion` command will be hidden, but still functional. ## Creating your own completion command