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

Add default completion command even if there are no other sub-commands #1559

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -1085,12 +1085,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

Expand All @@ -1102,9 +1096,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)
Expand Down
24 changes: 22 additions & 2 deletions completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just wondering what's the impact of this now taking a slice of args? Can this be made optional to still be backwards compatible for users who've adopted InitDefaultCompletionCmd? If it's fixing a bug / undefined behavior, that should be fine!

Copy link
Collaborator Author

@marckhouzam marckhouzam Jan 30, 2025

Choose a reason for hiding this comment

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

Oh good point. I don’t recall why this function was ever exported. But since cobra does not need to be exported, it must mean that we did that for other people to call it. So good call that this is a breaking change.

we could simply make the argument an ellipsis to avoid breaking compilation. However, I feel this would be worse for programs that don’t have sub commands because we will break their behaviour because this completion command will now appear unless they pass the arguments properly.

I could create a new function instead I guess. I’ll give it some thought and post an update

Thanks for catching this

if c.CompletionOptions.DisableDefaultCmd {
return
}

Expand All @@ -724,6 +724,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,
Expand All @@ -738,6 +742,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"
Expand Down
53 changes: 50 additions & 3 deletions completions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,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 {
Expand All @@ -2474,6 +2474,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,
Expand Down Expand Up @@ -2595,19 +2606,55 @@ 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,
}
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",
Expand Down
3 changes: 2 additions & 1 deletion site/content/completions/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading