Skip to content

Commit

Permalink
start
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com>
  • Loading branch information
marckhouzam committed Jun 19, 2023
1 parent fdee73b commit 10702a4
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 13 deletions.
15 changes: 8 additions & 7 deletions command.go
Expand Up @@ -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

Expand All @@ -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)
Expand Down
24 changes: 22 additions & 2 deletions completions.go
Expand Up @@ -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
}

Expand All @@ -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,
Expand All @@ -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"
Expand Down
53 changes: 50 additions & 3 deletions completions_test.go
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -2451,19 +2462,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 shell_completions.md
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

0 comments on commit 10702a4

Please sign in to comment.