Repository: spf13/cobra
Pull request #2180 (2024-08-24 03:38:53)

Fix --version help and output for plugins
When writing a plugin and setting CommandDisplayNameAnnotation you want the display name to be used in help text. This did not work when setting command Version since the help text and version templates used the command Name().

This change makes command DisplayName() public, so it can be used in templates or other code that want to use this value, and fix --version help text and template to use the command DisplayName().


nirs: Lint errors are fixed in https://github.com/spf13/cobra/pull/2181
marckhouzam: I’ll let you rebase this one first @nirs 
nirs: @marckhouzam can be merged now

Patch:
diff --git a/README.md b/README.md
index 6444f4b..0fb0373 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ go install github.com/spf13/cobra-cli@latest
 
 For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md)
 
-For complete details on using the Cobra library, please read the [The Cobra User Guide](site/content/user_guide.md).
+For complete details on using the Cobra library, please read [The Cobra User Guide](site/content/user_guide.md).
 
 # License
 
diff --git a/command.go b/command.go
index 2df6975..4cd712b 100644
--- a/command.go
+++ b/command.go
@@ -607,7 +607,7 @@ func (c *Command) VersionTemplate() string {
 	if c.HasParent() {
 		return c.parent.VersionTemplate()
 	}
-	return `{{with .Name}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
+	return `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
 `
 }
 
@@ -1190,7 +1190,7 @@ func (c *Command) InitDefaultHelpFlag() {
 	c.mergePersistentFlags()
 	if c.Flags().Lookup("help") == nil {
 		usage := "help for "
-		name := c.displayName()
+		name := c.DisplayName()
 		if name == "" {
 			usage += "this command"
 		} else {
@@ -1216,7 +1216,7 @@ func (c *Command) InitDefaultVersionFlag() {
 		if c.Name() == "" {
 			usage += "this command"
 		} else {
-			usage += c.Name()
+			usage += c.DisplayName()
 		}
 		if c.Flags().ShorthandLookup("v") == nil {
 			c.Flags().BoolP("version", "v", false, usage)
@@ -1240,7 +1240,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.displayName() + ` 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)
@@ -1431,10 +1431,12 @@ func (c *Command) CommandPath() string {
 	if c.HasParent() {
 		return c.Parent().CommandPath() + " " + c.Name()
 	}
-	return c.displayName()
+	return c.DisplayName()
 }
 
-func (c *Command) displayName() string {
+// DisplayName returns the name to display in help text. Returns command Name()
+// If CommandDisplayNameAnnoation is not set
+func (c *Command) DisplayName() string {
 	if displayName, ok := c.Annotations[CommandDisplayNameAnnotation]; ok {
 		return displayName
 	}
@@ -1444,7 +1446,7 @@ func (c *Command) displayName() 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)
+	use := strings.Replace(c.Use, c.Name(), c.DisplayName(), 1)
 	if c.HasParent() {
 		useline = c.parent.CommandPath() + " " + use
 	} else {
@@ -1650,7 +1652,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.displayName(), flag.ContinueOnError)
+		c.flags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1665,7 +1667,7 @@ func (c *Command) Flags() *flag.FlagSet {
 func (c *Command) LocalNonPersistentFlags() *flag.FlagSet {
 	persistentFlags := c.PersistentFlags()
 
-	out := flag.NewFlagSet(c.displayName(), 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)
@@ -1680,7 +1682,7 @@ func (c *Command) LocalFlags() *flag.FlagSet {
 	c.mergePersistentFlags()
 
 	if c.lflags == nil {
-		c.lflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
+		c.lflags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1708,7 +1710,7 @@ func (c *Command) InheritedFlags() *flag.FlagSet {
 	c.mergePersistentFlags()
 
 	if c.iflags == nil {
-		c.iflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
+		c.iflags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1737,7 +1739,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.displayName(), flag.ContinueOnError)
+		c.pflags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 		if c.flagErrorBuf == nil {
 			c.flagErrorBuf = new(bytes.Buffer)
 		}
@@ -1750,9 +1752,9 @@ func (c *Command) PersistentFlags() *flag.FlagSet {
 func (c *Command) ResetFlags() {
 	c.flagErrorBuf = new(bytes.Buffer)
 	c.flagErrorBuf.Reset()
-	c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
+	c.flags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 	c.flags.SetOutput(c.flagErrorBuf)
-	c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
+	c.pflags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 	c.pflags.SetOutput(c.flagErrorBuf)
 
 	c.lflags = nil
@@ -1869,7 +1871,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.displayName(), flag.ContinueOnError)
+		c.parentsPflags = flag.NewFlagSet(c.DisplayName(), flag.ContinueOnError)
 		c.parentsPflags.SetOutput(c.flagErrorBuf)
 		c.parentsPflags.SortFlags = false
 	}
diff --git a/site/content/active_help.md b/site/content/active_help.md
index d72acc7..1b02c70 100644
--- a/site/content/active_help.md
+++ b/site/content/active_help.md
@@ -2,19 +2,21 @@
 
 Active Help is a framework provided by Cobra which allows a program to define messages (hints, warnings, etc) that will be printed during program usage.  It aims to make it easier for your users to learn how to use your program.  If configured by the program, Active Help is printed when the user triggers shell completion.
 
-For example, 
-```
-bash-5.1$ helm repo add [tab]
+For example,
+
+```console
+$ helm repo add [tab]
 You must choose a name for the repo you are adding.
 
-bash-5.1$ bin/helm package [tab]
+$ bin/helm package [tab]
 Please specify the path to the chart to package
 
-bash-5.1$ bin/helm package [tab][tab]
+$ bin/helm package [tab][tab]
 bin/    internal/    scripts/    pkg/     testdata/
 ```
 
 **Hint**: A good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions to guide the user in knowing what is expected by the program.
+
 ## Supported shells
 
 Active Help is currently only supported for the following shells:
@@ -52,22 +54,26 @@ cmd := &cobra.Command{
 	},
 }
 ```
+
 The example above defines the completions (none, in this specific example) as well as the Active Help messages for the `helm repo add` command.  It yields the following behavior:
-```
-bash-5.1$ helm repo add [tab]
+
+```console
+$ helm repo add [tab]
 You must choose a name for the repo you are adding
 
-bash-5.1$ helm repo add grafana [tab]
+$ helm repo add grafana [tab]
 You must specify the URL for the repo you are adding
 
-bash-5.1$ helm repo add grafana https://grafana.github.io/helm-charts [tab]
+$ helm repo add grafana https://grafana.github.io/helm-charts [tab]
 This command does not take any more arguments
 ```
+
 **Hint**: As can be seen in the above example, a good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions.
 
 ### Active Help for flags
 
 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) {
 		if len(args) != 2 {
@@ -77,11 +83,12 @@ _ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []st
 	})
 ```
 The example above prints an Active Help message when not enough information was given by the user to complete the `--version` flag.
-```
-bash-5.1$ bin/helm install myrelease --version 2.0.[tab]
+
+```console
+$ bin/helm install myrelease --version 2.0.[tab]
 You must first specify the chart to install before the --version flag can be completed
 
-bash-5.1$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab]
+$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab]
 2.0.1  2.0.2  2.0.3
 ```
 
@@ -103,6 +110,7 @@ Active Help configuration using the `cobra.GetActiveHelpConfig(cmd)` function an
 should or should not be added (instead of reading the environment variable directly).
 
 For example:
+
 ```go
 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	activeHelpLevel := cobra.GetActiveHelpConfig(cmd)
@@ -124,11 +132,13 @@ ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([
 	return comps, cobra.ShellCompDirectiveNoFileComp
 },
 ```
+
 **Note 1**: If the `<PROGRAM>_ACTIVE_HELP` environment variable is set to the string "0", Cobra will automatically disable all Active Help output (even if some output was specified by the program using the `cobra.AppendActiveHelp(...)` function).  Using "0" can simplify your code in situations where you want to blindly disable Active Help without having to call `cobra.GetActiveHelpConfig(cmd)` explicitly.
 
 **Note 2**: If a user wants to disable Active Help for every single program based on Cobra, she can set the environment variable `COBRA_ACTIVE_HELP` to "0".  In this case `cobra.GetActiveHelpConfig(cmd)` will return "0" no matter what the variable `<PROGRAM>_ACTIVE_HELP` is set to.
 
 **Note 3**: If the user does not set `<PROGRAM>_ACTIVE_HELP` or `COBRA_ACTIVE_HELP` (which will be a common case), the default value for the Active Help configuration returned by `cobra.GetActiveHelpConfig(cmd)` will be the empty string. 
+
 ## Active Help with Cobra's default completion command
 
 Cobra provides a default `completion` command for programs that wish to use it.
@@ -141,7 +151,8 @@ details for your users.
 Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command.  Please refer to [debugging shell completion](shell_completions.md#debugging) for details.
 
 When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable.  That variable is named `<PROGRAM>_ACTIVE_HELP` where any non-ASCII-alphanumeric characters are replaced by an `_`.  For example, we can test deactivating some Active Help as shown below:
-```
+
+```console
 $ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h<ENTER>
 bitnami/haproxy
 bitnami/harbor
diff --git a/site/content/user_guide.md b/site/content/user_guide.md
index 93e87d6..dd26de4 100644
--- a/site/content/user_guide.md
+++ b/site/content/user_guide.md
@@ -3,14 +3,14 @@
 While you are welcome to provide your own organization, typically a Cobra-based
 application will follow the following organizational structure:
 
-```test
-  ▾ appName/
-    ▾ cmd/
-        add.go
-        your.go
-        commands.go
-        here.go
-      main.go
+```console
+▾ appName/
+  ▾ cmd/
+      add.go
+      your.go
+      commands.go
+      here.go
+  main.go
 ```
 
 In a Cobra app, typically the main.go file is very bare. It serves one purpose: initializing Cobra.
@@ -18,9 +18,7 @@ In a Cobra app, typically the main.go file is very bare. It serves one purpose:
 ```go
 package main
 
-import (
-  "{pathToYourApp}/cmd"
-)
+import "{pathToYourApp}/cmd"
 
 func main() {
   cmd.Execute()
@@ -148,9 +146,7 @@ In a Cobra app, typically the main.go file is very bare. It serves one purpose:
 ```go
 package main
 
-import (
-  "{pathToYourApp}/cmd"
-)
+import "{pathToYourApp}/cmd"
 
 func main() {
   cmd.Execute()
@@ -197,7 +193,7 @@ its own go package.
 The suggested approach is for the parent command to use `AddCommand` to add its most immediate
 subcommands. For example, consider the following directory structure:
 
-```text
+```console
 ├── cmd
 │   ├── root.go
 │   └── sub1
@@ -508,30 +504,31 @@ create' is called.  Every command will automatically have the '--help' flag adde
 The following output is automatically generated by Cobra. Nothing beyond the
 command and flag definitions are needed.
 
-    $ cobra-cli help
-
-    Cobra is a CLI library for Go that empowers applications.
-    This application is a tool to generate the needed files
-    to quickly create a Cobra application.
+```console
+$ cobra-cli help
 
-    Usage:
-      cobra-cli [command]
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
 
-    Available Commands:
-      add         Add a command to a Cobra Application
-      completion  Generate the autocompletion script for the specified shell
-      help        Help about any command
-      init        Initialize a Cobra Application
+Usage:
+  cobra-cli [command]
 
-    Flags:
-      -a, --author string    author name for copyright attribution (default "YOUR NAME")
-          --config string    config file (default is $HOME/.cobra.yaml)
-      -h, --help             help for cobra-cli
-      -l, --license string   name of license for the project
-          --viper            use Viper for configuration
+Available Commands:
+  add         Add a command to a Cobra Application
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  init        Initialize a Cobra Application
 
-    Use "cobra-cli [command] --help" for more information about a command.
+Flags:
+  -a, --author string    author name for copyright attribution (default "YOUR NAME")
+      --config string    config file (default is $HOME/.cobra.yaml)
+  -h, --help             help for cobra-cli
+  -l, --license string   name of license for the project
+      --viper            use Viper for configuration
 
+Use "cobra-cli [command] --help" for more information about a command.
+```
 
 Help is just a command like any other. There is no special logic or behavior
 around it. In fact, you can provide your own if you want.
@@ -566,25 +563,27 @@ showing the user the 'usage'.
 You may recognize this from the help above. That's because the default help
 embeds the usage as part of its output.
 
-    $ cobra-cli --invalid
-    Error: unknown flag: --invalid
-    Usage:
-      cobra-cli [command]
+```console
+$ cobra-cli --invalid
+Error: unknown flag: --invalid
+Usage:
+  cobra-cli [command]
 
-    Available Commands:
-      add         Add a command to a Cobra Application
-      completion  Generate the autocompletion script for the specified shell
-      help        Help about any command
-      init        Initialize a Cobra Application
+Available Commands:
+  add         Add a command to a Cobra Application
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  init        Initialize a Cobra Application
 
-    Flags:
-      -a, --author string    author name for copyright attribution (default "YOUR NAME")
-          --config string    config file (default is $HOME/.cobra.yaml)
-      -h, --help             help for cobra-cli
-      -l, --license string   name of license for the project
-          --viper            use Viper for configuration
+Flags:
+  -a, --author string    author name for copyright attribution (default "YOUR NAME")
+      --config string    config file (default is $HOME/.cobra.yaml)
+  -h, --help             help for cobra-cli
+  -l, --license string   name of license for the project
+      --viper            use Viper for configuration
 
-    Use "cobra [command] --help" for more information about a command.
+Use "cobra [command] --help" for more information about a command.
+```
 
 ### Defining your own usage
 You can provide your own usage function or template for Cobra to use.
@@ -701,7 +700,7 @@ Set `EnableTraverseRunHooks` global variable to `true` if you want to execute al
 
 Cobra will print automatic suggestions when "unknown command" errors happen. This allows Cobra to behave similarly to the `git` command when a typo happens. For example:
 
-```
+```console
 $ hugo srever
 Error: unknown command "srever" for "hugo"
 
@@ -728,7 +727,7 @@ command.SuggestionsMinimumDistance = 1
 You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but make sense in your set of commands but for which
 you don't want aliases. Example:
 
-```bash
+```console
 $ kubectl remove
 Error: unknown command "remove" for "kubectl"
 
@@ -793,7 +792,7 @@ func main() {
 
 Example run as a kubectl plugin:
 
-```bash
+```console
 $ kubectl myplugin
 Usage:
   kubectl myplugin [command]


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 21f81e0..ffdbf14 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -67,6 +67,7 @@ jobs:
         - 20
         - 21
         - 22
+        - 23
     name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x'
     runs-on: ${{ matrix.platform }}-latest
     steps:
diff --git a/command_test.go b/command_test.go
index 9ce7a52..cd44992 100644
--- a/command_test.go
+++ b/command_test.go
@@ -371,8 +371,9 @@ func TestAliasPrefixMatching(t *testing.T) {
 // text should reflect the way we run the command.
 func TestPlugin(t *testing.T) {
 	cmd := &Command{
-		Use:  "kubectl-plugin",
-		Args: NoArgs,
+		Use:     "kubectl-plugin",
+		Version: "1.0.0",
+		Args:    NoArgs,
 		Annotations: map[string]string{
 			CommandDisplayNameAnnotation: "kubectl plugin",
 		},
@@ -386,13 +387,15 @@ func TestPlugin(t *testing.T) {
 
 	checkStringContains(t, cmdHelp, "kubectl plugin [flags]")
 	checkStringContains(t, cmdHelp, "help for kubectl plugin")
+	checkStringContains(t, cmdHelp, "version for kubectl plugin")
 }
 
 // TestPlugin checks usage as plugin with sub commands.
 func TestPluginWithSubCommands(t *testing.T) {
 	rootCmd := &Command{
-		Use:  "kubectl-plugin",
-		Args: NoArgs,
+		Use:     "kubectl-plugin",
+		Version: "1.0.0",
+		Args:    NoArgs,
 		Annotations: map[string]string{
 			CommandDisplayNameAnnotation: "kubectl plugin",
 		},
@@ -408,6 +411,7 @@ func TestPluginWithSubCommands(t *testing.T) {
 
 	checkStringContains(t, rootHelp, "kubectl plugin [command]")
 	checkStringContains(t, rootHelp, "help for kubectl plugin")
+	checkStringContains(t, rootHelp, "version for kubectl plugin")
 	checkStringContains(t, rootHelp, "kubectl plugin [command] --help")
 
 	childHelp, err := executeCommand(rootCmd, "sub", "-h")
@@ -1090,6 +1094,24 @@ func TestVersionFlagExecuted(t *testing.T) {
 	checkStringContains(t, output, "root version 1.0.0")
 }
 
+func TestVersionFlagExecutedDiplayName(t *testing.T) {
+	rootCmd := &Command{
+		Use:     "kubectl-plugin",
+		Version: "1.0.0",
+		Annotations: map[string]string{
+			CommandDisplayNameAnnotation: "kubectl plugin",
+		},
+		Run: emptyRun,
+	}
+
+	output, err := executeCommand(rootCmd, "--version", "arg1")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	checkStringContains(t, output, "kubectl plugin version 1.0.0")
+}
+
 func TestVersionFlagExecutedWithNoName(t *testing.T) {
 	rootCmd := &Command{Version: "1.0.0", Run: emptyRun}