From bc8a1d0c80d3ebcb4273ec4bf76c2a1ee7033a0c Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sat, 11 Dec 2021 12:59:13 -0500 Subject: [PATCH] Remove the default "completion" cmd if it is alone 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. 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 --- command.go | 6 +++--- completions.go | 17 +++++++++++++++-- completions_test.go | 22 +++++++++++++--------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/command.go b/command.go index 2cc18891d..48bbc7642 100644 --- a/command.go +++ b/command.go @@ -929,8 +929,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() args := c.args @@ -939,7 +937,9 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { args = os.Args[1:] } - // initialize the hidden command to be used for shell completion + // initialize the default completion command + c.initDefaultCompletionCmd(args) + // initialize the __complete command to be used for shell completion c.initCompleteCmd(args) var flags []string diff --git a/completions.go b/completions.go index 6a839f99d..e269a1ccc 100644 --- a/completions.go +++ b/completions.go @@ -585,7 +585,7 @@ 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() { +func (c *Command) initDefaultCompletionCmd(args []string) { if c.CompletionOptions.DisableDefaultCmd { return } @@ -598,7 +598,8 @@ func (c *Command) initDefaultCompletionCmd() { } haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions - hidden := c.CompletionOptions.HiddenDefaultCmd || !c.HasSubCommands() + hasSubCommands := c.HasSubCommands() + hidden := c.CompletionOptions.HiddenDefaultCmd || !hasSubCommands completionCmd := &Command{ Use: compCmdName, @@ -612,6 +613,18 @@ 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 + // 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. + subCmd, _, err := c.Find(args) + if err != nil || subCmd.Name() != compCmdName { + // The completion command is not being called, remove it. + c.RemoveCommand(completionCmd) + } + } + 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 5137ab893..060a9fd62 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2287,22 +2287,26 @@ func TestDefaultCompletionCmd(t *testing.T) { Run: emptyRun, } - // Test that default completion command is created, hidden if there are no 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()) - found := false for _, cmd := range rootCmd.commands { if cmd.Name() == compCmdName { - found = true - if !cmd.Hidden { - t.Errorf("The 'completion' command should be hidden when there are no other sub-commands of root") - } + t.Errorf("Should not have a 'completion' command when there are no other sub-commands of root") break } } - if !found { - t.Errorf("Should have a 'completion' command when there are no other sub-commands of root") + + // 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, @@ -2310,7 +2314,7 @@ func TestDefaultCompletionCmd(t *testing.T) { rootCmd.AddCommand(subCmd) // Test that a completion command is created if there are other sub-commands - found = false + found := false assertNoErr(t, rootCmd.Execute()) for _, cmd := range rootCmd.commands { if cmd.Name() == compCmdName {