Repository: spf13/cobra
Pull request #2070 (2023-11-12 14:05:54)

Fixes and docs for usage as plugin
Using the new CommandDisplayNameAnnotation annotation introduced in Cobra 1.8.0.

Fixes help text issues with:
- help flag usage
- help command long description
- usage line for command without sub commands

nirs: Example runs using https://github.com/nirs/kubectl-cobraplugin

Root command help:

```
$ kubectl cobraplugin
Usage:
  kubectl cobraplugin [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  subcmd      

Flags:
  -h, --help   help for kubectl cobraplugin

Use "kubectl cobraplugin [command] --help" for more information about a command.
```

Sub command help:

```
$ kubectl cobraplugin subcmd -h
Usage:
  kubectl cobraplugin subcmd [flags]

Flags:
  -h, --help   help for subcmd
```

Help command help:
```
$ kubectl cobraplugin help -h
Help provides help for any command in the application.
Simply type kubectl cobraplugin help [path to command] for full details.

Usage:
  kubectl cobraplugin help [command] [flags]

Flags:
  -h, --help   help for help
```
nirs: Examples run with sample-cli-plugin:
https://github.com/nirs/kubernetes/tree/sample-cli-plugin-help

```
$ kubectl ns -h
View or set the current namespace

Usage:
  kubectl ns [new-namespace] [flags]

Examples:

	# view the current namespace in your KUBECONFIG
	kubectl ns

	# view all of the namespaces in use by contexts in your KUBECONFIG
	kubectl ns --list

	# switch your current-context to one that contains the desired namespace
	kubectl ns foo


Flags:
  -h, --help                           help for kubectl ns
  ...
```

Patch:
diff --git a/active_help.go b/active_help.go
index 5f965e0..25c30e3 100644
--- a/active_help.go
+++ b/active_help.go
@@ -17,21 +17,17 @@ package cobra
 import (
 	"fmt"
 	"os"
-	"regexp"
-	"strings"
 )
 
 const (
 	activeHelpMarker = "_activeHelp_ "
 	// The below values should not be changed: programs will be using them explicitly
 	// in their user documentation, and users will be using them explicitly.
-	activeHelpEnvVarSuffix  = "_ACTIVE_HELP"
-	activeHelpGlobalEnvVar  = "COBRA_ACTIVE_HELP"
+	activeHelpEnvVarSuffix  = "ACTIVE_HELP"
+	activeHelpGlobalEnvVar  = configEnvVarGlobalPrefix + "_" + activeHelpEnvVarSuffix
 	activeHelpGlobalDisable = "0"
 )
 
-var activeHelpEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`)
-
 // AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp.
 // Such strings will be processed by the completion script and will be shown as ActiveHelp
 // to the user.
@@ -60,8 +56,5 @@ func GetActiveHelpConfig(cmd *Command) string {
 // variable.  It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the
 // root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`.
 func activeHelpEnvVar(name string) string {
-	// This format should not be changed: users will be using it explicitly.
-	activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix))
-	activeHelpEnvVar = activeHelpEnvVarPrefixSubstRegexp.ReplaceAllString(activeHelpEnvVar, "_")
-	return activeHelpEnvVar
+	return configEnvVar(name, activeHelpEnvVarSuffix)
 }
diff --git a/args.go b/args.go
index e79ec33..ed1e70c 100644
--- a/args.go
+++ b/args.go
@@ -52,9 +52,9 @@ func OnlyValidArgs(cmd *Command, args []string) error {
 	if len(cmd.ValidArgs) > 0 {
 		// Remove any description that may be included in ValidArgs.
 		// A description is following a tab character.
-		var validArgs []string
+		validArgs := make([]string, 0, len(cmd.ValidArgs))
 		for _, v := range cmd.ValidArgs {
-			validArgs = append(validArgs, strings.Split(v, "\t")[0])
+			validArgs = append(validArgs, strings.SplitN(v, "\t", 2)[0])
 		}
 		for _, v := range args {
 			if !stringInSlice(v, validArgs) {
diff --git a/bash_completions.go b/bash_completions.go
index 8a53151..be83570 100644
--- a/bash_completions.go
+++ b/bash_completions.go
@@ -621,7 +621,7 @@ func writeRequiredNouns(buf io.StringWriter, cmd *Command) {
 	for _, value := range cmd.ValidArgs {
 		// Remove any description that may be included following a tab character.
 		// Descriptions are not supported by bash completion.
-		value = strings.Split(value, "\t")[0]
+		value = strings.SplitN(value, "\t", 2)[0]
 		WriteStringAndCheck(buf, fmt.Sprintf("    must_have_one_noun+=(%q)\n", value))
 	}
 	if cmd.ValidArgsFunction != nil {
diff --git a/cobra.go b/cobra.go
index a6b160c..e0b0947 100644
--- a/cobra.go
+++ b/cobra.go
@@ -193,8 +193,6 @@ func ld(s, t string, ignoreCase bool) int {
 	d := make([][]int, len(s)+1)
 	for i := range d {
 		d[i] = make([]int, len(t)+1)
-	}
-	for i := range d {
 		d[i][0] = i
 	}
 	for j := range d[0] {
diff --git a/command.go b/command.go
index 2fbe6c1..677dc9b 100644
--- a/command.go
+++ b/command.go
@@ -706,7 +706,7 @@ Loop:
 			// This is not a flag or a flag value. Check to see if it matches what we're looking for, and if so,
 			// return the args, excluding the one at this position.
 			if s == x {
-				ret := []string{}
+				ret := make([]string, 0, len(args)-1)
 				ret = append(ret, args[:pos]...)
 				ret = append(ret, args[pos+1:]...)
 				return ret
@@ -754,14 +754,14 @@ func (c *Command) findSuggestions(arg string) string {
 	if c.SuggestionsMinimumDistance <= 0 {
 		c.SuggestionsMinimumDistance = 2
 	}
-	suggestionsString := ""
+	var sb strings.Builder
 	if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 {
-		suggestionsString += "\n\nDid you mean this?\n"
+		sb.WriteString("\n\nDid you mean this?\n")
 		for _, s := range suggestions {
-			suggestionsString += fmt.Sprintf("\t%v\n", s)
+			_, _ = fmt.Fprintf(&sb, "\t%v\n", s)
 		}
 	}
-	return suggestionsString
+	return sb.String()
 }
 
 func (c *Command) findNext(next string) *Command {
@@ -1187,10 +1187,11 @@ func (c *Command) InitDefaultHelpFlag() {
 	c.mergePersistentFlags()
 	if c.Flags().Lookup("help") == nil {
 		usage := "help for "
-		if c.Name() == "" {
+		name := c.displayName()
+		if name == "" {
 			usage += "this command"
 		} else {
-			usage += c.Name()
+			usage += name
 		}
 		c.Flags().BoolP("help", "h", false, usage)
 		_ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"})
@@ -1236,7 +1237,7 @@ func (c *Command) InitDefaultHelpCmd() {
 			Use:   "help [command]",
 			Short: "Help about any command",
 			Long: `Help provides help for any command in the application.
-Simply type ` + c.Name() + ` help [path to command] for full details.`,
+Simply type ` + c.displayName() + ` help [path to command] for full details.`,
 			ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
 				var completions []string
 				cmd, _, e := c.Root().Find(args)
@@ -1427,6 +1428,10 @@ func (c *Command) CommandPath() string {
 	if c.HasParent() {
 		return c.Parent().CommandPath() + " " + c.Name()
 	}
+	return c.displayName()
+}
+
+func (c *Command) displayName() string {
 	if displayName, ok := c.Annotations[CommandDisplayNameAnnotation]; ok {
 		return displayName
 	}
@@ -1436,10 +1441,11 @@ func (c *Command) CommandPath() string {
 // UseLine puts out the full usage for a given command (including parents).
 func (c *Command) UseLine() string {
 	var useline string
+	use := strings.Replace(c.Use, c.Name(), c.displayName(), 1)
 	if c.HasParent() {
-		useline = c.parent.CommandPath() + " " + c.Use
+		useline = c.parent.CommandPath() + " " + use
 	} else {
-		useline = c.Use
+		useline = use
 	}
 	if c.DisableFlagsInUseLine {
 		return useline
@@ -1642,7 +1648,7 @@ func (c *Command) GlobalNormalizationFunc() func(f *flag.FlagSet, name string) f
 // to this command (local and persistent declared here and by all parents).
 func (c *Command) Flags() *flag.FlagSet {
 	if c.flags == nil {
-		c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+		c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1656,7 +1662,7 @@ func (c *Command) Flags() *flag.FlagSet {
 func (c *Command) LocalNonPersistentFlags() *flag.FlagSet {
 	persistentFlags := c.PersistentFlags()
 
-	out := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+	out := flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 	c.LocalFlags().VisitAll(func(f *flag.Flag) {
 		if persistentFlags.Lookup(f.Name) == nil {
 			out.AddFlag(f)
@@ -1670,7 +1676,7 @@ func (c *Command) LocalFlags() *flag.FlagSet {
 	c.mergePersistentFlags()
 
 	if c.lflags == nil {
-		c.lflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+		c.lflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1697,7 +1703,7 @@ func (c *Command) InheritedFlags() *flag.FlagSet {
 	c.mergePersistentFlags()
 
 	if c.iflags == nil {
-		c.iflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+		c.iflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1725,7 +1731,7 @@ func (c *Command) NonInheritedFlags() *flag.FlagSet {
 // PersistentFlags returns the persistent FlagSet specifically set in the current command.
 func (c *Command) PersistentFlags() *flag.FlagSet {
 	if c.pflags == nil {
-		c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+		c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1738,9 +1744,9 @@ func (c *Command) PersistentFlags() *flag.FlagSet {
 func (c *Command) ResetFlags() {
 	c.flagErrorBuf = new(bytes.Buffer)
 	c.flagErrorBuf.Reset()
-	c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+	c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 	c.flags.SetOutput(c.flagErrorBuf)
-	c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+	c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 	c.pflags.SetOutput(c.flagErrorBuf)
 
 	c.lflags = nil
@@ -1857,7 +1863,7 @@ func (c *Command) mergePersistentFlags() {
 // If c.parentsPflags == nil, it makes new.
 func (c *Command) updateParentsPflags() {
 	if c.parentsPflags == nil {
-		c.parentsPflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+		c.parentsPflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
 		c.parentsPflags.SetOutput(c.flagErrorBuf)
 		c.parentsPflags.SortFlags = false
 	}
diff --git a/completions.go b/completions.go
index b60f6b2..ad7b6d0 100644
--- a/completions.go
+++ b/completions.go
@@ -17,6 +17,8 @@ package cobra
 import (
 	"fmt"
 	"os"
+	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 
@@ -211,24 +213,29 @@ func (c *Command) initCompleteCmd(args []string) {
 				// 2- Even without completions, we need to print the directive
 			}
 
-			noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
+			noDescriptions := cmd.CalledAs() == ShellCompNoDescRequestCmd
+			if !noDescriptions {
+				if doDescriptions, err := strconv.ParseBool(getEnvConfig(cmd, configEnvVarSuffixDescriptions)); err == nil {
+					noDescriptions = !doDescriptions
+				}
+			}
+			noActiveHelp := GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable
+			out := finalCmd.OutOrStdout()
 			for _, comp := range completions {
-				if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable {
-					// Remove all activeHelp entries in this case
-					if strings.HasPrefix(comp, activeHelpMarker) {
-						continue
-					}
+				if noActiveHelp && strings.HasPrefix(comp, activeHelpMarker) {
+					// Remove all activeHelp entries if it's disabled.
+					continue
 				}
 				if noDescriptions {
 					// Remove any description that may be included following a tab character.
-					comp = strings.Split(comp, "\t")[0]
+					comp = strings.SplitN(comp, "\t", 2)[0]
 				}
 
 				// Make sure we only write the first line to the output.
 				// This is needed if a description contains a linebreak.
 				// Otherwise the shell scripts will interpret the other lines as new flags
 				// and could therefore provide a wrong completion.
-				comp = strings.Split(comp, "\n")[0]
+				comp = strings.SplitN(comp, "\n", 2)[0]
 
 				// Finally trim the completion.  This is especially important to get rid
 				// of a trailing tab when there are no description following it.
@@ -237,14 +244,14 @@ func (c *Command) initCompleteCmd(args []string) {
 				// although there is no description).
 				comp = strings.TrimSpace(comp)
 
-				// Print each possible completion to stdout for the completion script to consume.
-				fmt.Fprintln(finalCmd.OutOrStdout(), comp)
+				// Print each possible completion to the output for the completion script to consume.
+				fmt.Fprintln(out, comp)
 			}
 
 			// As the last printout, print the completion directive for the completion script to parse.
 			// The directive integer must be that last character following a single colon (:).
 			// The completion script expects :<directive>
-			fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
+			fmt.Fprintf(out, ":%d\n", directive)
 
 			// Print some helpful info to stderr for the user to understand.
 			// Output from stderr must be ignored by the completion script.
@@ -899,3 +906,34 @@ func CompError(msg string) {
 func CompErrorln(msg string) {
 	CompError(fmt.Sprintf("%s\n", msg))
 }
+
+// These values should not be changed: users will be using them explicitly.
+const (
+	configEnvVarGlobalPrefix       = "COBRA"
+	configEnvVarSuffixDescriptions = "COMPLETION_DESCRIPTIONS"
+)
+
+var configEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`)
+
+// configEnvVar returns the name of the program-specific configuration environment
+// variable.  It has the format <PROGRAM>_<SUFFIX> where <PROGRAM> is the name of the
+// root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`.
+func configEnvVar(name, suffix string) string {
+	// This format should not be changed: users will be using it explicitly.
+	v := strings.ToUpper(fmt.Sprintf("%s_%s", name, suffix))
+	v = configEnvVarPrefixSubstRegexp.ReplaceAllString(v, "_")
+	return v
+}
+
+// getEnvConfig returns the value of the configuration environment variable
+// <PROGRAM>_<SUFFIX> where <PROGRAM> is the name of the root command in upper
+// case, with all non-ASCII-alphanumeric characters replaced by `_`.
+// If the value is empty or not set, the value of the environment variable
+// COBRA_<SUFFIX> is returned instead.
+func getEnvConfig(cmd *Command, suffix string) string {
+	v := os.Getenv(configEnvVar(cmd.Root().Name(), suffix))
+	if v == "" {
+		v = os.Getenv(configEnvVar(configEnvVarGlobalPrefix, suffix))
+	}
+	return v
+}
diff --git a/flag_groups.go b/flag_groups.go
index 0671ec5..2be3b18 100644
--- a/flag_groups.go
+++ b/flag_groups.go
@@ -130,7 +130,7 @@ func processFlagForGroupAnnotation(flags *flag.FlagSet, pflag *flag.Flag, annota
 					continue
 				}
 
-				groupStatus[group] = map[string]bool{}
+				groupStatus[group] = make(map[string]bool, len(flagnames))
 				for _, name := range flagnames {
 					groupStatus[group][name] = false
 				}
@@ -253,17 +253,17 @@ func (c *Command) enforceFlagGroupsForCompletion() {
 	// If none of the flags of a one-required group are present, we make all the flags
 	// of that group required so that the shell completion suggests them automatically
 	for flagList, flagnameAndStatus := range oneRequiredGroupStatus {
-		set := 0
+		isSet := false
 
-		for _, isSet := range flagnameAndStatus {
+		for _, isSet = range flagnameAndStatus {
 			if isSet {
-				set++
+				break
 			}
 		}
 
 		// None of the flags of the group are set, mark all flags in the group
 		// as required
-		if set == 0 {
+		if !isSet {
 			for _, fName := range strings.Split(flagList, " ") {
 				_ = c.MarkFlagRequired(fName)
 			}
diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md
index 4efad29..02257ad 100644
--- a/site/content/completions/_index.md
+++ b/site/content/completions/_index.md
@@ -393,6 +393,9 @@ $ source <(helm completion bash --no-descriptions)
 $ helm completion [tab][tab]
 bash        fish        powershell  zsh
 ```
+
+Setting the `<PROGRAM>_COMPLETION_DESCRIPTIONS` environment variable (falling back to `COBRA_COMPLETION_DESCRIPTIONS` if empty or not set) to a [falsey value](https://pkg.go.dev/strconv#ParseBool) achieves the same. `<PROGRAM>` is the name of your program with all non-ASCII-alphanumeric characters replaced by `_`.
+
 ## Bash completions
 
 ### Dependencies
diff --git a/site/content/projects_using_cobra.md b/site/content/projects_using_cobra.md
index 8a291eb..73e7fea 100644
--- a/site/content/projects_using_cobra.md
+++ b/site/content/projects_using_cobra.md
@@ -12,6 +12,7 @@
 - [Datree](https://github.com/datreeio/datree)
 - [Delve](https://github.com/derekparker/delve)
 - [Docker (distribution)](https://github.com/docker/distribution)
+- [Encore](https://encore.dev)
 - [Etcd](https://etcd.io/)
 - [Gardener](https://github.com/gardener/gardenctl)
 - [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl)
@@ -30,6 +31,7 @@
 - [Kubescape](https://github.com/kubescape/kubescape)
 - [KubeVirt](https://github.com/kubevirt/kubevirt)
 - [Linkerd](https://linkerd.io/)
+- [LXC](https://github.com/canonical/lxd)
 - [Mattermost-server](https://github.com/mattermost/mattermost-server)
 - [Mercure](https://mercure.rocks/)
 - [Meroxa CLI](https://github.com/meroxa/cli)
diff --git a/site/content/user_guide.md b/site/content/user_guide.md
index 4116e8d..3b42ef0 100644
--- a/site/content/user_guide.md
+++ b/site/content/user_guide.md
@@ -748,3 +748,57 @@ Read more about it in [Shell Completions](completions/_index.md).
 Cobra makes use of the shell-completion system to define a framework allowing you to provide Active Help to your users.
 Active Help are messages (hints, warnings, etc) printed as the program is being used.
 Read more about it in [Active Help](active_help.md).
+
+## Creating a plugin
+
+When creating a plugin for tools like *kubectl*, the executable is named
+`kubectl-myplugin`, but it is used as `kubectl myplugin`. To fix help
+messages and completions, annotate the root command with the
+`cobra.CommandDisplayNameAnnotation` annotation.
+
+### Example kubectl plugin
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+func main() {
+	rootCmd := &cobra.Command{
+		Use: "kubectl-myplugin",
+		Annotations: map[string]string{
+			cobra.CommandDisplayNameAnnotation: "kubectl myplugin",
+		},
+	}
+	subCmd := &cobra.Command{
+		Use: "subcmd",
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Println("kubectl myplugin subcmd")
+		},
+	}
+	rootCmd.AddCommand(subCmd)
+	rootCmd.Execute()
+}
+```
+
+Example run as a kubectl plugin:
+
+```
+$ kubectl myplugin
+Usage:
+  kubectl myplugin [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  subcmd
+
+Flags:
+  -h, --help   help for kubectl myplugin
+
+Use "kubectl myplugin [command] --help" for more information about a command.
+```


Test patch:
diff --git a/command_test.go b/command_test.go
index 9f686d6..b7d88e4 100644
--- a/command_test.go
+++ b/command_test.go
@@ -370,8 +370,28 @@ func TestAliasPrefixMatching(t *testing.T) {
 // executable is `kubectl-plugin`, but we run it as `kubectl plugin`. The help
 // text should reflect the way we run the command.
 func TestPlugin(t *testing.T) {
+	cmd := &Command{
+		Use:  "kubectl-plugin",
+		Args: NoArgs,
+		Annotations: map[string]string{
+			CommandDisplayNameAnnotation: "kubectl plugin",
+		},
+		Run: emptyRun,
+	}
+
+	cmdHelp, err := executeCommand(cmd, "-h")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	checkStringContains(t, cmdHelp, "kubectl plugin [flags]")
+	checkStringContains(t, cmdHelp, "help for kubectl plugin")
+}
+
+// TestPlugin checks usage as plugin with sub commands.
+func TestPluginWithSubCommands(t *testing.T) {
 	rootCmd := &Command{
-		Use:  "plugin",
+		Use:  "kubectl-plugin",
 		Args: NoArgs,
 		Annotations: map[string]string{
 			CommandDisplayNameAnnotation: "kubectl plugin",
@@ -387,6 +407,8 @@ func TestPlugin(t *testing.T) {
 	}
 
 	checkStringContains(t, rootHelp, "kubectl plugin [command]")
+	checkStringContains(t, rootHelp, "help for kubectl plugin")
+	checkStringContains(t, rootHelp, "kubectl plugin [command] --help")
 
 	childHelp, err := executeCommand(rootCmd, "sub", "-h")
 	if err != nil {
@@ -394,6 +416,15 @@ func TestPlugin(t *testing.T) {
 	}
 
 	checkStringContains(t, childHelp, "kubectl plugin sub [flags]")
+	checkStringContains(t, childHelp, "help for sub")
+
+	helpHelp, err := executeCommand(rootCmd, "help", "-h")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	checkStringContains(t, helpHelp, "kubectl plugin help [path to command]")
+	checkStringContains(t, helpHelp, "kubectl plugin help [command]")
 }
 
 // TestChildSameName checks the correct behaviour of cobra in cases,
diff --git a/completions_test.go b/completions_test.go
index d5aee25..df153fc 100644
--- a/completions_test.go
+++ b/completions_test.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"os"
 	"strings"
 	"sync"
 	"testing"
@@ -3517,3 +3518,194 @@ func TestGetFlagCompletion(t *testing.T) {
 		})
 	}
 }
+
+func TestGetEnvConfig(t *testing.T) {
+	testCases := []struct {
+		desc      string
+		use       string
+		suffix    string
+		cmdVar    string
+		globalVar string
+		cmdVal    string
+		globalVal string
+		expected  string
+	}{
+		{
+			desc:      "Command envvar overrides global",
+			use:       "root",
+			suffix:    "test",
+			cmdVar:    "ROOT_TEST",
+			globalVar: "COBRA_TEST",
+			cmdVal:    "cmd",
+			globalVal: "global",
+			expected:  "cmd",
+		},
+		{
+			desc:      "Missing/empty command envvar falls back to global",
+			use:       "root",
+			suffix:    "test",
+			cmdVar:    "ROOT_TEST",
+			globalVar: "COBRA_TEST",
+			cmdVal:    "",
+			globalVal: "global",
+			expected:  "global",
+		},
+		{
+			desc:      "Missing/empty command and global envvars fall back to empty",
+			use:       "root",
+			suffix:    "test",
+			cmdVar:    "ROOT_TEST",
+			globalVar: "COBRA_TEST",
+			cmdVal:    "",
+			globalVal: "",
+			expected:  "",
+		},
+		{
+			desc:      "Periods in command use transform to underscores in env var name",
+			use:       "foo.bar",
+			suffix:    "test",
+			cmdVar:    "FOO_BAR_TEST",
+			globalVar: "COBRA_TEST",
+			cmdVal:    "cmd",
+			globalVal: "global",
+			expected:  "cmd",
+		},
+		{
+			desc:      "Dashes in command use transform to underscores in env var name",
+			use:       "quux-BAZ",
+			suffix:    "test",
+			cmdVar:    "QUUX_BAZ_TEST",
+			globalVar: "COBRA_TEST",
+			cmdVal:    "cmd",
+			globalVal: "global",
+			expected:  "cmd",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			// Could make env handling cleaner with t.Setenv with Go >= 1.17
+			err := os.Setenv(tc.cmdVar, tc.cmdVal)
+			defer func() {
+				assertNoErr(t, os.Unsetenv(tc.cmdVar))
+			}()
+			assertNoErr(t, err)
+			err = os.Setenv(tc.globalVar, tc.globalVal)
+			defer func() {
+				assertNoErr(t, os.Unsetenv(tc.globalVar))
+			}()
+			assertNoErr(t, err)
+			cmd := &Command{Use: tc.use}
+			got := getEnvConfig(cmd, tc.suffix)
+			if got != tc.expected {
+				t.Errorf("expected: %q, got: %q", tc.expected, got)
+			}
+		})
+	}
+}
+
+func TestDisableDescriptions(t *testing.T) {
+	rootCmd := &Command{
+		Use: "root",
+		Run: emptyRun,
+	}
+
+	childCmd := &Command{
+		Use:   "thechild",
+		Short: "The child command",
+		Run:   emptyRun,
+	}
+	rootCmd.AddCommand(childCmd)
+
+	specificDescriptionsEnvVar := configEnvVar(rootCmd.Name(), configEnvVarSuffixDescriptions)
+	globalDescriptionsEnvVar := configEnvVar(configEnvVarGlobalPrefix, configEnvVarSuffixDescriptions)
+
+	const (
+		descLineWithDescription    = "first\tdescription"
+		descLineWithoutDescription = "first"
+	)
+	childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
+		comps := []string{descLineWithDescription}
+		return comps, ShellCompDirectiveDefault
+	}
+
+	testCases := []struct {
+		desc             string
+		globalEnvValue   string
+		specificEnvValue string
+		expectedLine     string
+	}{
+		{
+			"No env variables set",
+			"",
+			"",
+			descLineWithDescription,
+		},
+		{
+			"Global value false",
+			"false",
+			"",
+			descLineWithoutDescription,
+		},
+		{
+			"Specific value false",
+			"",
+			"false",
+			descLineWithoutDescription,
+		},
+		{
+			"Both values false",
+			"false",
+			"false",
+			descLineWithoutDescription,
+		},
+		{
+			"Both values true",
+			"true",
+			"true",
+			descLineWithDescription,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			if err := os.Setenv(specificDescriptionsEnvVar, tc.specificEnvValue); err != nil {
+				t.Errorf("Unexpected error setting %s: %v", specificDescriptionsEnvVar, err)
+			}
+			if err := os.Setenv(globalDescriptionsEnvVar, tc.globalEnvValue); err != nil {
+				t.Errorf("Unexpected error setting %s: %v", globalDescriptionsEnvVar, err)
+			}
+
+			var run = func() {
+				output, err := executeCommand(rootCmd, ShellCompRequestCmd, "thechild", "")
+				if err != nil {
+					t.Errorf("Unexpected error: %v", err)
+				}
+
+				expected := strings.Join([]string{
+					tc.expectedLine,
+					":0",
+					"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
+				if output != expected {
+					t.Errorf("expected: %q, got: %q", expected, output)
+				}
+			}
+
+			run()
+
+			// For empty cases, test also unset state
+			if tc.specificEnvValue == "" {
+				if err := os.Unsetenv(specificDescriptionsEnvVar); err != nil {
+					t.Errorf("Unexpected error unsetting %s: %v", specificDescriptionsEnvVar, err)
+				}
+				run()
+			}
+			if tc.globalEnvValue == "" {
+				if err := os.Unsetenv(globalDescriptionsEnvVar); err != nil {
+					t.Errorf("Unexpected error unsetting %s: %v", globalDescriptionsEnvVar, err)
+				}
+				run()
+			}
+		})
+	}
+}