Skip to content

Commit

Permalink
Remove __complete cmd for program without subcmds (#1563)
Browse files Browse the repository at this point in the history
Fixes #1562

Programs that don't have sub-commands can accept any number of args.
However, when doing shell completion for such programs, within the
__complete code this very __complete command makes it that the program
suddenly has a sub-command, and the call to Find() -> legacyArgs() will
then return an error if there are more than one argument on the
command-line being completed.

To avoid this, we first remove the __complete command in such a case so
as to get back to having no sub-commands.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
  • Loading branch information
marckhouzam committed Dec 14, 2021
1 parent 19c9c74 commit 9054739
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 1 deletion.
12 changes: 11 additions & 1 deletion completions.go
Expand Up @@ -228,7 +228,17 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
if c.Root().TraverseChildren {
finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs)
} else {
finalCmd, finalArgs, err = c.Root().Find(trimmedArgs)
// For Root commands that don't specify any value for their Args fields, when we call
// Find(), if those Root commands don't have any sub-commands, they will accept arguments.
// However, because we have added the __complete sub-command in the current code path, the
// call to Find() -> legacyArgs() will return an error if there are any arguments.
// To avoid this, we first remove the __complete command to get back to having no sub-commands.
rootCmd := c.Root()
if len(rootCmd.Commands()) == 1 {
rootCmd.RemoveCommand(c)
}

finalCmd, finalArgs, err = rootCmd.Find(trimmedArgs)
}
if err != nil {
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
Expand Down
45 changes: 45 additions & 0 deletions completions_test.go
Expand Up @@ -2619,3 +2619,48 @@ func TestCompleteWithDisableFlagParsing(t *testing.T) {
t.Errorf("expected: %q, got: %q", expected, output)
}
}

func TestCompleteWithRootAndLegacyArgs(t *testing.T) {
// Test a lonely root command which uses legacyArgs(). In such a case, the root
// command should accept any number of arguments and completion should behave accordingly.
rootCmd := &Command{
Use: "root",
Args: nil, // Args must be nil to trigger the legacyArgs() function
Run: emptyRun,
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
return []string{"arg1", "arg2"}, ShellCompDirectiveNoFileComp
},
}

// Make sure the first arg is completed
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected := strings.Join([]string{
"arg1",
"arg2",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}

// Make sure the completion of arguments continues
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "arg1", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected = strings.Join([]string{
"arg1",
"arg2",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}

0 comments on commit 9054739

Please sign in to comment.