Repository: spf13/cobra
Pull request #1559 (2021-12-11 19:41:44)

Add default completion command even if there are no other sub-commands
This supersedes #1450 and #1392

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).

@scop can you confirm this is what you were hoping for?

**Testing done**

```
# Here is a program with no sub-cmds
$ cat tests/minimal.go
package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use: "prog",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("value:", value)
		fmt.Println("args:", args)
	},
}

var value string

func main() {
	rootCmd.Flags().StringVarP(&value, "value", "e", "", "value")
	rootCmd.Execute()
}

$ go build -o minimal ./tests/minimal.go

$ ./minimal
value:
args: []
$ ./minimal arg1
value:
args: [arg1]
$ ./minimal arg1 arg2
value:
args: [arg1 arg2]
$ ./minimal arg1 arg2 --value 1
value: 1
args: [arg1 arg2]

# Notice no 'completion' command visible
$ ./minimal -h
Usage:
  prog [flags]

Flags:
  -h, --help           help for prog
  -e, --value string   value

# But the completion command does exist
$ ./minimal completion -h
Generate the autocompletion script for prog for the specified shell.
See each sub-command's help for details on how to use the generated script.

Usage:
  prog completion [command]

Available Commands:
  bash        Generate the autocompletion script for bash
  fish        Generate the autocompletion script for fish
  powershell  Generate the autocompletion script for powershell
  zsh         Generate the autocompletion script for zsh

Flags:
  -h, --help   help for completion

Use "prog completion [command] --help" for more information about a command.

# Shell completion for the 'completion' command itself is not triggered,
# but it is triggered for sub-commands of 'completion'
$ ./minimal __complete compl
:0
Completion ended with directive: ShellCompDirectiveDefault
$ ./minimal __complete completion ''
bash	Generate the autocompletion script for bash
fish	Generate the autocompletion script for fish
powershell	Generate the autocompletion script for powershell
zsh	Generate the autocompletion script for zsh
:4
Completion ended with directive: ShellCompDirectiveNoFileComp
```


scop: Yep, seems to work nicely, thanks!

An observation is that as the `completion` command is hidden, no completions are generated for its arguments (i.e. "foo completion <kbd>Tab</kbd>"), but that's such a minor detail and not a result of this change I believe, that let's not get stuck with that :)
marckhouzam: > An observation is that as the `completion` command is hidden, no completions are generated for its arguments (i.e. "foo completion Tab")

Nice attention to detail! The completions should work for hidden commands.  In this case however, when we call the "__complete" command to get the completions, we don't create the "completion" command so its subcommands don't get completed.

Let me try to see if I can fix it.


marckhouzam: While working on this I ran into another completion bug for programs that don't have sub-commands.
I've opened issue #1562 and posted a fix in #1563.

I'm still looking at fixing the issue @scop reported above.
caarlos0: This looks good, was just about to dig into this myself.

Thanks!
marckhouzam: I've let this rot for too long. Let's plan it for the next release. Come August iI will run some tests on it and make sure it is good.
caarlos0: anything I can do to help getting this merged?
marckhouzam: I've finally re-worked this and I believe it is ready.
Please refer to the description of the PR to see the testing that I did, which shows the new behaviour.
marckhouzam: @jpmcb When you have moment, this is ready for review.  Don't hesitate to ask for clarifications.
marckhouzam: > So looks like this was unintended behavior where the default completions were only available when some other commands were provided?

It’s the default “completion” _command_ that doesn’t get created unless other commands exist. 

Say you write a tool called `tooly`. If `tooly` has any sub commands (e.g., `tooly list`) then cobra will automatically create `tooly completion` which allow users to generate the shell completion scripts. 

This PR makes this happen for tools that don’t have any sub commands


marckhouzam: @jpmcb This is ready now.  Thanks again for catching the breaking change.

Patch:
diff --git a/README.md b/README.md
index 0fb0373..7175715 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-![cobra logo](assets/CobraMain.png)
+
+![cobra logo](https://github.com/user-attachments/assets/cbc3adf8-0dff-46e9-a88d-5e2d971c169e)
 
 Cobra is a library for creating powerful modern CLI applications.
 
diff --git a/active_help.go b/active_help.go
index 25c30e3..b3e2dad 100644
--- a/active_help.go
+++ b/active_help.go
@@ -35,7 +35,7 @@ const (
 // This function can be called multiple times before and/or after completions are added to
 // the array.  Each time this function is called with the same array, the new
 // ActiveHelp line will be shown below the previous ones when completion is triggered.
-func AppendActiveHelp(compArray []string, activeHelpStr string) []string {
+func AppendActiveHelp(compArray []Completion, activeHelpStr string) []Completion {
 	return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr))
 }
 
diff --git a/command.go b/command.go
index 60b9c45..fbefc05 100644
--- a/command.go
+++ b/command.go
@@ -82,7 +82,7 @@ type Command struct {
 	Example string
 
 	// ValidArgs is list of all valid non-flag arguments that are accepted in shell completions
-	ValidArgs []string
+	ValidArgs []Completion
 	// ValidArgsFunction is an optional function that provides valid non-flag arguments for shell completion.
 	// It is a dynamic version of using ValidArgs.
 	// Only one of ValidArgs and ValidArgsFunction can be used for a command.
@@ -1097,12 +1097,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
 
@@ -1114,9 +1108,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)
@@ -1272,8 +1273,8 @@ func (c *Command) InitDefaultHelpCmd() {
 			Short: "Help about any command",
 			Long: `Help provides help for any command in the application.
 Simply type ` + c.DisplayName() + ` help [path to command] for full details.`,
-			ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
-				var completions []string
+			ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
+				var completions []Completion
 				cmd, _, e := c.Root().Find(args)
 				if e != nil {
 					return nil, ShellCompDirectiveNoFileComp
@@ -1285,7 +1286,7 @@ Simply type ` + c.DisplayName() + ` help [path to command] for full details.`,
 				for _, subCmd := range cmd.Commands() {
 					if subCmd.IsAvailableCommand() || subCmd == cmd.helpCommand {
 						if strings.HasPrefix(subCmd.Name(), toComplete) {
-							completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
+							completions = append(completions, CompletionWithDesc(subCmd.Name(), subCmd.Short))
 						}
 					}
 				}
diff --git a/completions.go b/completions.go
index cd899c7..cedb4d7 100644
--- a/completions.go
+++ b/completions.go
@@ -117,15 +117,31 @@ type CompletionOptions struct {
 	HiddenDefaultCmd bool
 }
 
+// Completion is a string that can be used for completions
+//
+// two formats are supported:
+//   - the completion choice
+//   - the completion choice with a textual description (separated by a TAB).
+//
+// [CompletionWithDesc] can be used to create a completion string with a textual description.
+//
+// Note: Go type alias is used to provide a more descriptive name in the documentation, but any string can be used.
+type Completion = string
+
 // CompletionFunc is a function that provides completion results.
-type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
+type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective)
+
+// CompletionWithDesc returns a [Completion] with a description by using the TAB delimited format.
+func CompletionWithDesc(choice string, description string) Completion {
+	return choice + "\t" + description
+}
 
 // NoFileCompletions can be used to disable file completion for commands that should
 // not trigger file completions.
 //
 // This method satisfies [CompletionFunc].
 // It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
-func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
+func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
 	return nil, ShellCompDirectiveNoFileComp
 }
 
@@ -134,8 +150,8 @@ func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string
 //
 // This method returns a function that satisfies [CompletionFunc]
 // It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
-func FixedCompletions(choices []string, directive ShellCompDirective) CompletionFunc {
-	return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
+func FixedCompletions(choices []Completion, directive ShellCompDirective) CompletionFunc {
+	return func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) {
 		return choices, directive
 	}
 }
@@ -290,7 +306,7 @@ type SliceValue interface {
 	GetSlice() []string
 }
 
-func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
+func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCompDirective, error) {
 	// The last argument, which is not completely typed by the user,
 	// should not be part of the list of arguments
 	toComplete := args[len(args)-1]
@@ -318,7 +334,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 	}
 	if err != nil {
 		// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
-		return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
+		return c, []Completion{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
 	}
 	finalCmd.ctx = c.ctx
 
@@ -348,7 +364,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 
 	// Parse the flags early so we can check if required flags are set
 	if err = finalCmd.ParseFlags(finalArgs); err != nil {
-		return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
+		return finalCmd, []Completion{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
 	}
 
 	realArgCount := finalCmd.Flags().NArg()
@@ -360,14 +376,14 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 	if flagErr != nil {
 		// If error type is flagCompError and we don't want flagCompletion we should ignore the error
 		if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
-			return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
+			return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr
 		}
 	}
 
 	// Look for the --help or --version flags.  If they are present,
 	// there should be no further completions.
 	if helpOrVersionFlagPresent(finalCmd) {
-		return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
+		return finalCmd, []Completion{}, ShellCompDirectiveNoFileComp, nil
 	}
 
 	// We only remove the flags from the arguments if DisableFlagParsing is not set.
@@ -396,11 +412,11 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 				return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
 			}
 			// Directory completion
-			return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
+			return finalCmd, []Completion{}, ShellCompDirectiveFilterDirs, nil
 		}
 	}
 
-	var completions []string
+	var completions []Completion
 	var directive ShellCompDirective
 
 	// Enforce flag groups before doing flag completions
@@ -486,7 +502,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 				for _, subCmd := range finalCmd.Commands() {
 					if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
 						if strings.HasPrefix(subCmd.Name(), toComplete) {
-							completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
+							completions = append(completions, CompletionWithDesc(subCmd.Name(), subCmd.Short))
 						}
 						directive = ShellCompDirectiveNoFileComp
 					}
@@ -542,7 +558,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 	if completionFn != nil {
 		// Go custom completion defined for this flag or command.
 		// Call the registered completion function to get the completions.
-		var comps []string
+		var comps []Completion
 		comps, directive = completionFn(finalCmd, finalArgs, toComplete)
 		completions = append(completions, comps...)
 	}
@@ -562,16 +578,16 @@ func helpOrVersionFlagPresent(cmd *Command) bool {
 	return false
 }
 
-func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
+func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []Completion {
 	if nonCompletableFlag(flag) {
-		return []string{}
+		return []Completion{}
 	}
 
-	var completions []string
+	var completions []Completion
 	flagName := "--" + flag.Name
 	if strings.HasPrefix(flagName, toComplete) {
 		// Flag without the =
-		completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
+		completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
 
 		// Why suggest both long forms: --flag and --flag= ?
 		// This forces the user to *always* have to type either an = or a space after the flag name.
@@ -583,20 +599,20 @@ func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
 		// if len(flag.NoOptDefVal) == 0 {
 		// 	// Flag requires a value, so it can be suffixed with =
 		// 	flagName += "="
-		// 	completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
+		// 	completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
 		// }
 	}
 
 	flagName = "-" + flag.Shorthand
 	if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
-		completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
+		completions = append(completions, CompletionWithDesc(flagName, flag.Usage))
 	}
 
 	return completions
 }
 
-func completeRequireFlags(finalCmd *Command, toComplete string) []string {
-	var completions []string
+func completeRequireFlags(finalCmd *Command, toComplete string) []Completion {
+	var completions []Completion
 
 	doCompleteRequiredFlags := func(flag *pflag.Flag) {
 		if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {
@@ -711,8 +727,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
 	}
 
@@ -725,6 +741,16 @@ func (c *Command) InitDefaultCompletionCmd() {
 
 	haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions
 
+	// Special case to know if there are sub-commands or not.
+	hasSubCommands := false
+	for _, cmd := range c.commands {
+		if cmd.Name() != ShellCompRequestCmd && cmd.Name() != helpCommandName {
+			// We found a real sub-command (not 'help' or '__complete')
+			hasSubCommands = true
+			break
+		}
+	}
+
 	completionCmd := &Command{
 		Use:   compCmdName,
 		Short: "Generate the autocompletion script for the specified shell",
@@ -738,6 +764,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,
+		// 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"
diff --git a/site/content/active_help.md b/site/content/active_help.md
index ae6d723..4ec8a59 100644
--- a/site/content/active_help.md
+++ b/site/content/active_help.md
@@ -41,8 +41,8 @@ cmd := &cobra.Command{
 	RunE: func(cmd *cobra.Command, args []string) error {
 		return addRepo(args)
 	},
-	ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-		var comps []string
+	ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
+		var comps []cobra.Completion
 		if len(args) == 0 {
 			comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
 		} else if len(args) == 1 {
@@ -75,7 +75,7 @@ This command does not take any more arguments
 Providing Active Help for flags is done in the same fashion as for nouns, but using the completion function registered for the flag.  For example:
 
 ```go
-_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
 		if len(args) != 2 {
 			return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp
 		}
@@ -112,10 +112,10 @@ should or should not be added (instead of reading the environment variable direc
 For example:
 
 ```go
-ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
 	activeHelpLevel := cobra.GetActiveHelpConfig(cmd)
 
-	var comps []string
+	var comps []cobra.Completion
 	if len(args) == 0 {
 		if activeHelpLevel != "off"  {
 			comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md
index 02257ad..5996707 100644
--- a/site/content/completions/_index.md
+++ b/site/content/completions/_index.md
@@ -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
 
@@ -177,7 +178,7 @@ cmd := &cobra.Command{
 	RunE: func(cmd *cobra.Command, args []string) {
 		RunGet(args[0])
 	},
-	ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
 		if len(args) != 0 {
 			return nil, cobra.ShellCompDirectiveNoFileComp
 		}
@@ -211,7 +212,7 @@ ShellCompDirectiveNoFileComp
 
 // Indicates that the returned completions should be used as file extension filters.
 // For example, to complete only files of the form *.json or *.yaml:
-//    return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt
+//    return []cobra.Completion{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
 // For flags, using MarkFlagFilename() and MarkPersistentFlagFilename()
 // is a shortcut to using this directive explicitly.
 //
@@ -219,13 +220,13 @@ ShellCompDirectiveFilterFileExt
 
 // Indicates that only directory names should be provided in file completion.
 // For example:
-//    return nil, ShellCompDirectiveFilterDirs
+//    return nil, cobra.ShellCompDirectiveFilterDirs
 // For flags, using MarkFlagDirname() is a shortcut to using this directive explicitly.
 //
 // To request directory names within another directory, the returned completions
 // should specify a single directory name within which to search. For example,
 // to complete directories within "themes/":
-//    return []string{"themes"}, ShellCompDirectiveFilterDirs
+//    return []cobra.Completion{"themes"}, cobra.ShellCompDirectiveFilterDirs
 //
 ShellCompDirectiveFilterDirs
 
@@ -293,8 +294,8 @@ As for nouns, Cobra provides a way of defining dynamic completion of flags.  To
 
 ```go
 flagName := "output"
-cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	return []string{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault
+cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
+	return []cobra.Completion{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault
 })
 ```
 Notice that calling `RegisterFlagCompletionFunc()` is done through the `command` with which the flag is associated.  In our example this dynamic completion will give results like so:
@@ -327,8 +328,8 @@ cmd.MarkFlagFilename(flagName, "yaml", "json")
 or
 ```go
 flagName := "output"
-cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt})
+cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
+	return []cobra.Completion{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt})
 ```
 
 ### Limit flag completions to directory names
@@ -341,15 +342,15 @@ cmd.MarkFlagDirname(flagName)
 or
 ```go
 flagName := "output"
-cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
 	return nil, cobra.ShellCompDirectiveFilterDirs
 })
 ```
 To limit completions of flag values to directory names *within another directory* you can use a combination of `RegisterFlagCompletionFunc()` and `ShellCompDirectiveFilterDirs` like so:
 ```go
 flagName := "output"
-cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	return []string{"themes"}, cobra.ShellCompDirectiveFilterDirs
+cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
+	return []cobra.Completion{"themes"}, cobra.ShellCompDirectiveFilterDirs
 })
 ```
 ### Descriptions for completions
@@ -370,15 +371,21 @@ $ helm s[tab]
 search  (search for a keyword in charts)  show  (show information of a chart)  status  (displays the status of the named release)
 ```
 
-Cobra allows you to add descriptions to your own completions.  Simply add the description text after each completion, following a `\t` separator.  This technique applies to completions returned by `ValidArgs`, `ValidArgsFunction` and `RegisterFlagCompletionFunc()`.  For example:
+Cobra allows you to add descriptions to your own completions.  Simply add the description text after each completion, following a `\t` separator. Cobra provides the helper function `CompletionWithDesc(string, string)` to create a completion with a description. This technique applies to completions returned by `ValidArgs`, `ValidArgsFunction` and `RegisterFlagCompletionFunc()`.  For example:
 ```go
-ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	return []string{"harbor\tAn image registry", "thanos\tLong-term metrics"}, cobra.ShellCompDirectiveNoFileComp
+ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
+	return []cobra.Completion{
+		cobra.CompletionWithDesc("harbor", "An image registry"),
+		cobra.CompletionWithDesc("thanos", "Long-term metrics")
+		}, cobra.ShellCompDirectiveNoFileComp
 }}
 ```
 or
 ```go
-ValidArgs: []string{"bash\tCompletions for bash", "zsh\tCompletions for zsh"}
+ValidArgs: []cobra.Completion{
+	cobra.CompletionWithDesc("bash", "Completions for bash"),
+	cobra.CompletionWithDesc("zsh", "Completions for zsh")
+	}
 ```
 
 If you don't want to show descriptions in the completions, you can add `--no-descriptions` to the default `completion` command to disable them, like:


Test patch:
diff --git a/completions_test.go b/completions_test.go
index a8f378e..964e389 100644
--- a/completions_test.go
+++ b/completions_test.go
@@ -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 {
@@ -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,
@@ -2595,6 +2606,42 @@ 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,
@@ -2602,12 +2649,12 @@ func TestCompleteCompletion(t *testing.T) {
 	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",
@@ -2872,6 +2919,56 @@ func TestFixedCompletions(t *testing.T) {
 	}
 }
 
+func TestFixedCompletionsWithCompletionHelpers(t *testing.T) {
+	rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
+	// here we are mixing string, [Completion] and [CompletionWithDesc]
+	choices := []string{"apple", Completion("banana"), CompletionWithDesc("orange", "orange are orange")}
+	childCmd := &Command{
+		Use:               "child",
+		ValidArgsFunction: FixedCompletions(choices, ShellCompDirectiveNoFileComp),
+		Run:               emptyRun,
+	}
+	rootCmd.AddCommand(childCmd)
+
+	t.Run("completion with description", func(t *testing.T) {
+		output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "a")
+		if err != nil {
+			t.Errorf("Unexpected error: %v", err)
+		}
+
+		expected := strings.Join([]string{
+			"apple",
+			"banana",
+			"orange\torange are orange", // this one has the description as expected with [ShellCompRequestCmd] flag
+			":4",
+			"Completion ended with directive: ShellCompDirectiveNoFileComp", "",
+		}, "\n")
+
+		if output != expected {
+			t.Errorf("expected: %q, got: %q", expected, output)
+		}
+	})
+
+	t.Run("completion with no description", func(t *testing.T) {
+		output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "a")
+		if err != nil {
+			t.Errorf("Unexpected error: %v", err)
+		}
+
+		expected := strings.Join([]string{
+			"apple",
+			"banana",
+			"orange", // the description is absent as expected with [ShellCompNoDescRequestCmd] flag
+			":4",
+			"Completion ended with directive: ShellCompDirectiveNoFileComp", "",
+		}, "\n")
+
+		if output != expected {
+			t.Errorf("expected: %q, got: %q", expected, output)
+		}
+	})
+}
+
 func TestCompletionForGroupedFlags(t *testing.T) {
 	getCmd := func() *Command {
 		rootCmd := &Command{
@@ -3742,3 +3839,94 @@ func TestDisableDescriptions(t *testing.T) {
 		})
 	}
 }
+
+// A test to make sure the InitDefaultCompletionCmd function works as expected
+// in case a project calls it directly.
+func TestInitDefaultCompletionCmd(t *testing.T) {
+
+	testCases := []struct {
+		desc          string
+		hasChildCmd   bool
+		args          []string
+		expectCompCmd bool
+	}{
+		{
+			desc:          "no child command and not calling the completion command",
+			hasChildCmd:   false,
+			args:          []string{"somearg"},
+			expectCompCmd: false,
+		},
+		{
+			desc:          "no child command but calling the completion command",
+			hasChildCmd:   false,
+			args:          []string{"completion"},
+			expectCompCmd: true,
+		},
+		{
+			desc:          "no child command but calling __complete on the root command",
+			hasChildCmd:   false,
+			args:          []string{"__complete", ""},
+			expectCompCmd: false,
+		},
+		{
+			desc:          "no child command but calling __complete on the completion command",
+			hasChildCmd:   false,
+			args:          []string{"__complete", "completion", ""},
+			expectCompCmd: true,
+		},
+		{
+			desc:          "with child command",
+			hasChildCmd:   true,
+			args:          []string{"child"},
+			expectCompCmd: true,
+		},
+		{
+			desc:          "no child command not passing args",
+			hasChildCmd:   false,
+			args:          nil,
+			expectCompCmd: false,
+		},
+		{
+			desc:          "with child command not passing args",
+			hasChildCmd:   true,
+			args:          nil,
+			expectCompCmd: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			rootCmd := &Command{Use: "root", Run: emptyRun}
+			childCmd := &Command{Use: "child", Run: emptyRun}
+
+			expectedNumSubCommands := 0
+			if tc.hasChildCmd {
+				rootCmd.AddCommand(childCmd)
+				expectedNumSubCommands++
+			}
+
+			if tc.expectCompCmd {
+				expectedNumSubCommands++
+			}
+
+			if len(tc.args) > 0 && tc.args[0] == "__complete" {
+				expectedNumSubCommands++
+			}
+
+			// Setup the __complete command to mimic real world scenarios
+			rootCmd.initCompleteCmd(tc.args)
+
+			// Call the InitDefaultCompletionCmd function directly
+			if tc.args == nil {
+				rootCmd.InitDefaultCompletionCmd()
+			} else {
+				rootCmd.InitDefaultCompletionCmd(tc.args...)
+			}
+
+			// Check if the completion command was added
+			if len(rootCmd.Commands()) != expectedNumSubCommands {
+				t.Errorf("Expected %d subcommands, got %d", expectedNumSubCommands, len(rootCmd.Commands()))
+			}
+		})
+	}
+}