Skip to content

Commit

Permalink
Remove the default "completion" cmd if it is alone
Browse files Browse the repository at this point in the history
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 <marc.khouzam@montreal.ca>
  • Loading branch information
marckhouzam committed Dec 11, 2021
1 parent c7cee2a commit bc8a1d0
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 14 deletions.
6 changes: 3 additions & 3 deletions command.go
Expand Up @@ -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

Expand All @@ -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
Expand Down
17 changes: 15 additions & 2 deletions completions.go
Expand Up @@ -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
}
Expand All @@ -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,
Expand All @@ -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"
Expand Down
22 changes: 13 additions & 9 deletions completions_test.go
Expand Up @@ -2287,30 +2287,34 @@ 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,
}
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 {
Expand Down

0 comments on commit bc8a1d0

Please sign in to comment.