Repository: spf13/cobra
Pull request #1956 (2023-05-05 11:49:51)

Allow linker to perform deadcode elimination for program using Cobra
Fixes #2015 

Cobra, in its default configuration, will execute a template to generate help, usage and version outputs. Text/template execution calls MethodByName and MethodByName disables dead code elimination in the Go linker, therefore all programs that make use of cobra will be linked with dead code elimination disabled, even if they end up replacing the default usage, help and version formatters with a custom function and no actual text/template evaluations are ever made at runtime.

Dead code elimination in the linker helps reduce disk space and memory utilization of programs. For example, for the simple example program used by TestDeadcodeElimination 40% of the final executable size is dead code. For a more realistic example, 12% of the size of Delve's executable is deadcode.

This PR changes Cobra so that, in its default configuration, it does not automatically inhibit deadcode elimination by:

1. changing Cobra's default behavior to emit output for usage and help using simple Go functions instead of template execution
2. quarantining all calls to template execution into SetUsageTemplate, SetHelpTemplate and SetVersionTemplate so that the linker can statically determine if they are reachable

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/spf13/cobra?pullRequest=1956) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/spf13/cobra?pullRequest=1956) before we can accept your contribution.<br/><hr/>**a** seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/spf13/cobra?pullRequest=1956) it.</sub>
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
aarzilli: The CLA thing isn't working for some reason.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
aarzilli: Unless I made a mistake there shouldn't be any behavior changes (as in, observable from the outside). Happy to explain anything about this if needed.
aarzilli: Ping?
aarzilli: Ping?
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
marckhouzam: @aarzilli I apologize for such a long delay, but I had misunderstood the value of this PR.  I now realize that it may help all programs using Cobra, so I'm very interested.

I'm trying to convince myself that that this change actually has an impact.
Would you be able to explain how I can see the difference in a program that has dead code, before and after this PR?

Here is what I tried.
- I used the program you have added to the tests in this PR
- I added an "Unused()" function that I don't call to that program.
- I ran `go build -o myprog .` (using cobra 1.8.1 *without* your PR
- I ran `go tool nm ./myprog | grep Unused`

I would have expected to see the `Unused` function but I don't.
Could you clarify what I should expect?

This is the program:
```
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

func Unused() {
	fmt.Println("not called")
}

var rootCmd = &cobra.Command{
	Version: "1.0",
	Use:     "example_program",
	Short:   "example_program - test fixture to check that deadcode elimination is allowed",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("hello world")
	},
	Aliases: []string{"alias1", "alias2"},
	Example: "stringer --help",
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
		os.Exit(1)
	}
}
```

aarzilli: One easy way to see the impact of this change is to compile your example program with and without this PR:

```
$ ls -lh
total 8.4M
-rwxr-xr-x. 1 a a 5.2M Oct 14 14:45 example.withoutpr*
-rwxr-xr-x. 1 a a 3.2M Oct 14 14:45 example.withpr*
-rw-r--r--. 1 a a  596 Oct 14 14:44 main.go
```

This is an extreme example where the size of the executable is reduced by 38% but reductions of 10% are realistic.
I use go1.23 but you should get very similar results with go1.22.8.

As to your question, saying that deadcode elimination gets disabled is incorrect: in reality it continues to function but in a reduced capacity. Specifically if `reflect.Value.MethodByName` or `reflect.Value.Method` are reachable it can not always prove that exported methods are unreachable.

Your Unused function is not an exported method so, unless it is called by an exported method, it can always be deadcode eliminated.

If you want to see the Unused function make it all the way to the executable you have to do something like this:

```
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

type Astruct struct {
	n int
}

//go:noinline
func Unused() {
	fmt.Println("not called")
}

//go:noinline
func (a *Astruct) Unused() {
	fmt.Println("not called", a.n)
	Unused()
}

//go:noinline
func (a *Astruct) Used() {
	fmt.Println("used", a.n)
}

var rootCmd = &cobra.Command{
	Version: "1.0",
	Use:     "example_program",
	Short:   "example_program - test fixture to check that deadcode elimination is allowed",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("hello world")
	},
	Aliases: []string{"alias1", "alias2"},
	Example: "stringer --help",
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
		os.Exit(1)
	}
	var a Astruct
	fmt.Println("%s", a)
	a.Used()
}
```

All of the noinline directives are needed because small functions like this would be removed by the inliner and the fmt.Println call is needed to make the Astruct type itself reachable (if all calls to the methods of Astruct are static the linker can still prove that Unused is unreachable).

With these changes you get:

```
$ go build main2.go && go tool nm ./main2 | grep 'Unused'
  589c00 T main.(*Astruct).Unused
  589ba0 T main.Unused
  765570 D runtime.adviseUnused
  4179e0 T runtime.sysUnusedOS

```

without the PR and:

```
$ go build main2.go && go tool nm ./main2 | grep 'Unused'
  602564 D runtime.adviseUnused
  4144a0 T runtime.sysUnusedOS

```

with the PR.
marckhouzam: Thanks for the explanation @aarzilli, I can now see the benefits.

So, with this PR, programs that don't set new templates (usage, help or version) will be able to do dead code elimination fully.

What about programs that do modify those templates?  If they want to get full usage of dead code elimination they should convert their template use to a go function like you have done, IIUC?   My next step for this review is to try to override the help/usage/version in that fashion and see if it works as expected.

I believe that using `SetHelpFunc()` and `SetUsageFunc()` will allow programs to do this, but I don't think we have a way to do this for the version template.  But it is ok, it is still better than not being able to do it at all.  Once this is merged, we can discuss adding a `SetVersionFunc()`.   
marckhouzam: What do you think about updating the documentation in the three sections starting here https://github.com/spf13/cobra/blob/main/site/content/user_guide.md#defining-your-own-help to explain the side-effect of overriding the template, and therefore that it is recommended to set a function instead?
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
aarzilli: > Thanks for the explanation @aarzilli, I can now see the benefits.
> 
> So, with this PR, programs that don't set new templates (usage, help or version) will be able to do dead code elimination fully.
> 
> What about programs that do modify those templates? If they want to get full usage of dead code elimination they should convert their template use to a go function like you have done, IIUC?

Yes, this is correct. 

> I believe that using `SetHelpFunc()` and `SetUsageFunc()` will allow programs to do this, but I don't think we have a way to do this for the version template. But it is ok, it is still better than not being able to do it at all. Once this is merged, we can discuss adding a `SetVersionFunc()`.

It imagine should be trivial.


github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
marckhouzam: The linter has failed

I looked in detail into this and it makes sense.
The problem is that in reality, so many projects use templates, that I wonder if any real program will benefit from this improvement?

For example, programs that use some k8s libraries will still end up using templates. I’ve tested this with both tanzu and helm and the binary size didn’t change. 

So I was just hesitant in accepting this because it complicates the Cobra code base and prevents any future use of templates. But it also feels wrong to force any program using cobra to have a dependency on templates. 

@aarzilli maybe you can convince me. Did you say Delve would be able to reduce its size with this change?
dominiquelefevre: > The problem is that in reality, so many projects use templates, that I wonder if any real program will benefit from this improvement?

At my company, most of services see a reduction of their size by 40% to 45% when DCE is enabled.

> For example, programs that use some k8s libraries will still end up using templates

As for k8s, most of it depends on `text/template` only indirectly via `x/net/trace`. Its package init func registers some complex HTTP handlers that use templating. That causes other problems as well, and I think that fixing the init will fix the dependency for k8s, too. See https://github.com/golang/go/issues/69951.

I think that k8s should not influence the design of other libs. There is a huge go ecosystem outside k8s.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
aarzilli: > @aarzilli maybe you can convince me. Did you say Delve would be able to reduce its size with this change?

Delve uses templates (and reflection) in various places but cobra is the only place where it couldn't be easily circumvented.

I think you are right that any given large Go program is likely to end up having multiple different dependencies on reflection but cobra itself is not large, nor is it only recommended for large programs.

You say "prevents any future use of templates" but I don't think it's true: first if they aren't statically reachable they are allowed, secondly this decision could be reversed if a compelling use case for templates was found.
pgimalac: To weight in, this change (and a few other similar ones in other dependencies) would reduce the size of various binaries I work on by 18 to 26% !

Despite using hundreds of dependencies, in particular from `k8s`, `docker`, `opentelemetry`, or `trivy`, I only had to change 6 (including `cobra`) to make it work, so it seems this is not that widespread.

Some known libraries provide explicit workarounds to avoid the import, eg. [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) uses [golang.org/x/net/trace](https://pkg.go.dev/golang.org/x/net/trace), which disables the optimization, but they provide a build tag to remove the import (`grpcnotrace`).
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
marckhouzam: On MinGW were getting a CI error

START| DeadcodeElimination
     |     cobra_test.go:291: could not run go tool nm: exit status 1
FAIL | DeadcodeElimination (0.56s)

we could actually just turn that test off for windows.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
github-actions[bot]: This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.
aarzilli: The changes look good to me.

Patch:
diff --git a/cobra.go b/cobra.go
index e0b0947..d9cd241 100644
--- a/cobra.go
+++ b/cobra.go
@@ -176,12 +176,16 @@ func rpad(s string, padding int) string {
 	return fmt.Sprintf(formattedString, s)
 }
 
-// tmpl executes the given template text on data, writing the result to w.
-func tmpl(w io.Writer, text string, data interface{}) error {
-	t := template.New("top")
-	t.Funcs(templateFuncs)
-	template.Must(t.Parse(text))
-	return t.Execute(w, data)
+func tmpl(text string) *tmplFunc {
+	return &tmplFunc{
+		tmpl: text,
+		fn: func(w io.Writer, data interface{}) error {
+			t := template.New("top")
+			t.Funcs(templateFuncs)
+			template.Must(t.Parse(text))
+			return t.Execute(w, data)
+		},
+	}
 }
 
 // ld compares two strings and returns the levenshtein distance between them.
diff --git a/command.go b/command.go
index 58f071a..6904bfb 100644
--- a/command.go
+++ b/command.go
@@ -32,6 +32,9 @@ import (
 const (
 	FlagSetByCobraAnnotation     = "cobra_annotation_flag_set_by_cobra"
 	CommandDisplayNameAnnotation = "cobra_annotation_command_display_name"
+
+	helpFlagName    = "help"
+	helpCommandName = "help"
 )
 
 // FParseErrWhitelist configures Flag parse errors to be ignored
@@ -167,12 +170,12 @@ type Command struct {
 	// usageFunc is usage func defined by user.
 	usageFunc func(*Command) error
 	// usageTemplate is usage template defined by user.
-	usageTemplate string
+	usageTemplate *tmplFunc
 	// flagErrorFunc is func defined by user and it's called when the parsing of
 	// flags returns an error.
 	flagErrorFunc func(*Command, error) error
 	// helpTemplate is help template defined by user.
-	helpTemplate string
+	helpTemplate *tmplFunc
 	// helpFunc is help func defined by user.
 	helpFunc func(*Command, []string)
 	// helpCommand is command with usage 'help'. If it's not defined by user,
@@ -185,7 +188,7 @@ type Command struct {
 	completionCommandGroupID string
 
 	// versionTemplate is the version template defined by user.
-	versionTemplate string
+	versionTemplate *tmplFunc
 
 	// errPrefix is the error message prefix defined by user.
 	errPrefix string
@@ -312,7 +315,7 @@ func (c *Command) SetUsageFunc(f func(*Command) error) {
 
 // SetUsageTemplate sets usage template. Can be defined by Application.
 func (c *Command) SetUsageTemplate(s string) {
-	c.usageTemplate = s
+	c.usageTemplate = tmpl(s)
 }
 
 // SetFlagErrorFunc sets a function to generate an error when flag parsing
@@ -348,12 +351,12 @@ func (c *Command) SetCompletionCommandGroupID(groupID string) {
 
 // SetHelpTemplate sets help template to be used. Application can use it to set custom template.
 func (c *Command) SetHelpTemplate(s string) {
-	c.helpTemplate = s
+	c.helpTemplate = tmpl(s)
 }
 
 // SetVersionTemplate sets version template to be used. Application can use it to set custom template.
 func (c *Command) SetVersionTemplate(s string) {
-	c.versionTemplate = s
+	c.versionTemplate = tmpl(s)
 }
 
 // SetErrPrefix sets error message prefix to be used. Application can use it to set custom prefix.
@@ -434,7 +437,8 @@ func (c *Command) UsageFunc() (f func(*Command) error) {
 	}
 	return func(c *Command) error {
 		c.mergePersistentFlags()
-		err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c)
+		fn := c.getUsageTemplateFunc()
+		err := fn(c.OutOrStderr(), c)
 		if err != nil {
 			c.PrintErrln(err)
 		}
@@ -442,6 +446,19 @@ func (c *Command) UsageFunc() (f func(*Command) error) {
 	}
 }
 
+// getUsageTemplateFunc returns the usage template function for the command
+// going up the command tree if necessary.
+func (c *Command) getUsageTemplateFunc() func(w io.Writer, data interface{}) error {
+	if c.usageTemplate != nil {
+		return c.usageTemplate.fn
+	}
+
+	if c.HasParent() {
+		return c.parent.getUsageTemplateFunc()
+	}
+	return defaultUsageFunc
+}
+
 // Usage puts out the usage for the command.
 // Used when a user provides invalid input.
 // Can be defined by user by overriding UsageFunc.
@@ -460,15 +477,30 @@ func (c *Command) HelpFunc() func(*Command, []string) {
 	}
 	return func(c *Command, a []string) {
 		c.mergePersistentFlags()
+		fn := c.getHelpTemplateFunc()
 		// The help should be sent to stdout
 		// See https://github.com/spf13/cobra/issues/1002
-		err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c)
+		err := fn(c.OutOrStdout(), c)
 		if err != nil {
 			c.PrintErrln(err)
 		}
 	}
 }
 
+// getHelpTemplateFunc returns the help template function for the command
+// going up the command tree if necessary.
+func (c *Command) getHelpTemplateFunc() func(w io.Writer, data interface{}) error {
+	if c.helpTemplate != nil {
+		return c.helpTemplate.fn
+	}
+
+	if c.HasParent() {
+		return c.parent.getHelpTemplateFunc()
+	}
+
+	return defaultHelpFunc
+}
+
 // Help puts out the help for the command.
 // Used when a user calls help [command].
 // Can be defined by user by overriding HelpFunc.
@@ -543,71 +575,55 @@ func (c *Command) NamePadding() int {
 }
 
 // UsageTemplate returns usage template for the command.
+// This function is kept for backwards-compatibility reasons.
 func (c *Command) UsageTemplate() string {
-	if c.usageTemplate != "" {
-		return c.usageTemplate
+	if c.usageTemplate != nil {
+		return c.usageTemplate.tmpl
 	}
 
 	if c.HasParent() {
 		return c.parent.UsageTemplate()
 	}
-	return `Usage:{{if .Runnable}}
-  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
-  {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
-
-Aliases:
-  {{.NameAndAliases}}{{end}}{{if .HasExample}}
-
-Examples:
-{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
-
-Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
-  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
-
-{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
-  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
-
-Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
-  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
-
-Flags:
-{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
-
-Global Flags:
-{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
-
-Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
-  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
-
-Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
-`
+	return defaultUsageTemplate
 }
 
 // HelpTemplate return help template for the command.
+// This function is kept for backwards-compatibility reasons.
 func (c *Command) HelpTemplate() string {
-	if c.helpTemplate != "" {
-		return c.helpTemplate
+	if c.helpTemplate != nil {
+		return c.helpTemplate.tmpl
 	}
 
 	if c.HasParent() {
 		return c.parent.HelpTemplate()
 	}
-	return `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}
-
-{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
+	return defaultHelpTemplate
 }
 
 // VersionTemplate return version template for the command.
+// This function is kept for backwards-compatibility reasons.
 func (c *Command) VersionTemplate() string {
-	if c.versionTemplate != "" {
-		return c.versionTemplate
+	if c.versionTemplate != nil {
+		return c.versionTemplate.tmpl
 	}
 
 	if c.HasParent() {
 		return c.parent.VersionTemplate()
 	}
-	return `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
-`
+	return defaultVersionTemplate
+}
+
+// getVersionTemplateFunc returns the version template function for the command
+// going up the command tree if necessary.
+func (c *Command) getVersionTemplateFunc() func(w io.Writer, data interface{}) error {
+	if c.versionTemplate != nil {
+		return c.versionTemplate.fn
+	}
+
+	if c.HasParent() {
+		return c.parent.getVersionTemplateFunc()
+	}
+	return defaultVersionFunc
 }
 
 // ErrPrefix return error message prefix for the command
@@ -894,7 +910,7 @@ func (c *Command) execute(a []string) (err error) {
 
 	// If help is called, regardless of other flags, return we want help.
 	// Also say we need help if the command isn't runnable.
-	helpVal, err := c.Flags().GetBool("help")
+	helpVal, err := c.Flags().GetBool(helpFlagName)
 	if err != nil {
 		// should be impossible to get here as we always declare a help
 		// flag in InitDefaultHelpFlag()
@@ -914,7 +930,8 @@ func (c *Command) execute(a []string) (err error) {
 			return err
 		}
 		if versionVal {
-			err := tmpl(c.OutOrStdout(), c.VersionTemplate(), c)
+			fn := c.getVersionTemplateFunc()
+			err := fn(c.OutOrStdout(), c)
 			if err != nil {
 				c.Println(err)
 			}
@@ -1190,7 +1207,7 @@ func (c *Command) checkCommandGroups() {
 // If c already has help flag, it will do nothing.
 func (c *Command) InitDefaultHelpFlag() {
 	c.mergePersistentFlags()
-	if c.Flags().Lookup("help") == nil {
+	if c.Flags().Lookup(helpFlagName) == nil {
 		usage := "help for "
 		name := c.DisplayName()
 		if name == "" {
@@ -1198,8 +1215,8 @@ func (c *Command) InitDefaultHelpFlag() {
 		} else {
 			usage += name
 		}
-		c.Flags().BoolP("help", "h", false, usage)
-		_ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"})
+		c.Flags().BoolP(helpFlagName, "h", false, usage)
+		_ = c.Flags().SetAnnotation(helpFlagName, FlagSetByCobraAnnotation, []string{"true"})
 	}
 }
 
@@ -1899,3 +1916,141 @@ func commandNameMatches(s string, t string) bool {
 
 	return s == t
 }
+
+// tmplFunc holds a template and a function that will execute said template.
+type tmplFunc struct {
+	tmpl string
+	fn   func(io.Writer, interface{}) error
+}
+
+var defaultUsageTemplate = `Usage:{{if .Runnable}}
+  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
+  {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
+
+Aliases:
+  {{.NameAndAliases}}{{end}}{{if .HasExample}}
+
+Examples:
+{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
+
+Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
+  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
+
+{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
+  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
+
+Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
+  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
+
+Flags:
+{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
+
+Global Flags:
+{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
+
+Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
+  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
+
+Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
+`
+
+// defaultUsageFunc is equivalent to executing defaultUsageTemplate. The two should be changed in sync.
+func defaultUsageFunc(w io.Writer, in interface{}) error {
+	c := in.(*Command)
+	fmt.Fprint(w, "Usage:")
+	if c.Runnable() {
+		fmt.Fprintf(w, "\n  %s", c.UseLine())
+	}
+	if c.HasAvailableSubCommands() {
+		fmt.Fprintf(w, "\n  %s [command]", c.CommandPath())
+	}
+	if len(c.Aliases) > 0 {
+		fmt.Fprintf(w, "\n\nAliases:\n")
+		fmt.Fprintf(w, "  %s", c.NameAndAliases())
+	}
+	if c.HasExample() {
+		fmt.Fprintf(w, "\n\nExamples:\n")
+		fmt.Fprintf(w, "%s", c.Example)
+	}
+	if c.HasAvailableSubCommands() {
+		cmds := c.Commands()
+		if len(c.Groups()) == 0 {
+			fmt.Fprintf(w, "\n\nAvailable Commands:")
+			for _, subcmd := range cmds {
+				if subcmd.IsAvailableCommand() || subcmd.Name() == helpCommandName {
+					fmt.Fprintf(w, "\n  %s %s", rpad(subcmd.Name(), subcmd.NamePadding()), subcmd.Short)
+				}
+			}
+		} else {
+			for _, group := range c.Groups() {
+				fmt.Fprintf(w, "\n\n%s", group.Title)
+				for _, subcmd := range cmds {
+					if subcmd.GroupID == group.ID && (subcmd.IsAvailableCommand() || subcmd.Name() == helpCommandName) {
+						fmt.Fprintf(w, "\n  %s %s", rpad(subcmd.Name(), subcmd.NamePadding()), subcmd.Short)
+					}
+				}
+			}
+			if !c.AllChildCommandsHaveGroup() {
+				fmt.Fprintf(w, "\n\nAdditional Commands:")
+				for _, subcmd := range cmds {
+					if subcmd.GroupID == "" && (subcmd.IsAvailableCommand() || subcmd.Name() == helpCommandName) {
+						fmt.Fprintf(w, "\n  %s %s", rpad(subcmd.Name(), subcmd.NamePadding()), subcmd.Short)
+					}
+				}
+			}
+		}
+	}
+	if c.HasAvailableLocalFlags() {
+		fmt.Fprintf(w, "\n\nFlags:\n")
+		fmt.Fprint(w, trimRightSpace(c.LocalFlags().FlagUsages()))
+	}
+	if c.HasAvailableInheritedFlags() {
+		fmt.Fprintf(w, "\n\nGlobal Flags:\n")
+		fmt.Fprint(w, trimRightSpace(c.InheritedFlags().FlagUsages()))
+	}
+	if c.HasHelpSubCommands() {
+		fmt.Fprintf(w, "\n\nAdditional help topcis:")
+		for _, subcmd := range c.Commands() {
+			if subcmd.IsAdditionalHelpTopicCommand() {
+				fmt.Fprintf(w, "\n  %s %s", rpad(subcmd.CommandPath(), subcmd.CommandPathPadding()), subcmd.Short)
+			}
+		}
+	}
+	if c.HasAvailableSubCommands() {
+		fmt.Fprintf(w, "\n\nUse \"%s [command] --help\" for more information about a command.", c.CommandPath())
+	}
+	fmt.Fprintln(w)
+	return nil
+}
+
+var defaultHelpTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}
+
+{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
+
+// defaultHelpFunc is equivalent to executing defaultHelpTemplate. The two should be changed in sync.
+func defaultHelpFunc(w io.Writer, in interface{}) error {
+	c := in.(*Command)
+	usage := c.Long
+	if usage == "" {
+		usage = c.Short
+	}
+	usage = trimRightSpace(usage)
+	if usage != "" {
+		fmt.Fprintln(w, usage)
+		fmt.Fprintln(w)
+	}
+	if c.Runnable() || c.HasSubCommands() {
+		fmt.Fprint(w, c.UsageString())
+	}
+	return nil
+}
+
+var defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
+`
+
+// defaultVersionFunc is equivalent to executing defaultVersionTemplate. The two should be changed in sync.
+func defaultVersionFunc(w io.Writer, in interface{}) error {
+	c := in.(*Command)
+	_, err := fmt.Fprintf(w, "%s version %s\n", c.DisplayName(), c.Version)
+	return err
+}
diff --git a/completions.go b/completions.go
index f7be8c7..cd899c7 100644
--- a/completions.go
+++ b/completions.go
@@ -555,7 +555,7 @@ func helpOrVersionFlagPresent(cmd *Command) bool {
 		len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed {
 		return true
 	}
-	if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil &&
+	if helpFlag := cmd.Flags().Lookup(helpFlagName); helpFlag != nil &&
 		len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed {
 		return true
 	}
diff --git a/site/content/user_guide.md b/site/content/user_guide.md
index dd26de4..3f61882 100644
--- a/site/content/user_guide.md
+++ b/site/content/user_guide.md
@@ -554,6 +554,9 @@ cmd.SetHelpTemplate(s string)
 
 The latter two will also apply to any children commands.
 
+Note that templates specified with `SetHelpTemplate` are evaluated using
+`text/template` which can increase the size of the compiled executable.
+
 ## Usage Message
 
 When the user provides an invalid flag or invalid command, Cobra responds by
@@ -586,6 +589,7 @@ 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.
 Like help, the function and template are overridable through public methods:
 
@@ -594,6 +598,9 @@ cmd.SetUsageFunc(f func(*Command) error)
 cmd.SetUsageTemplate(s string)
 ```
 
+Note that templates specified with `SetUsageTemplate` are evaluated using
+`text/template` which can increase the size of the compiled executable.
+
 ## Version Flag
 
 Cobra adds a top-level '--version' flag if the Version field is set on the root command.
@@ -601,6 +608,9 @@ Running an application with the '--version' flag will print the version to stdou
 the version template. The template can be customized using the
 `cmd.SetVersionTemplate(s string)` function.
 
+Note that templates specified with `SetVersionTemplate` are evaluated using
+`text/template` which can increase the size of the compiled executable.
+
 ## Error Message Prefix
 
 Cobra prints an error message when receiving a non-nil error value.


Test patch:
diff --git a/cobra_test.go b/cobra_test.go
index 2bba461..f1c5b0a 100644
--- a/cobra_test.go
+++ b/cobra_test.go
@@ -15,6 +15,11 @@
 package cobra
 
 import (
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
 	"testing"
 	"text/template"
 )
@@ -222,3 +227,75 @@ func TestRpad(t *testing.T) {
 		})
 	}
 }
+
+// TestDeadcodeElimination checks that a simple program using cobra in its
+// default configuration is linked taking full advantage of the linker's
+// deadcode elimination step.
+//
+// If reflect.Value.MethodByName/reflect.Value.Method are reachable the
+// linker will not always be able to prove that exported methods are
+// unreachable, making deadcode elimination less effective. Using
+// text/template and html/template makes reflect.Value.MethodByName
+// reachable.
+// Since cobra can use text/template templates this test checks that in its
+// default configuration that code path can be proven to be unreachable by
+// the linker.
+//
+// See also: https://github.com/spf13/cobra/pull/1956
+func TestDeadcodeElimination(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("go tool nm fails on windows")
+	}
+
+	// check that a simple program using cobra in its default configuration is
+	// linked with deadcode elimination enabled.
+	const (
+		dirname  = "test_deadcode"
+		progname = "test_deadcode_elimination"
+	)
+	_ = os.Mkdir(dirname, 0770)
+	defer os.RemoveAll(dirname)
+	filename := filepath.Join(dirname, progname+".go")
+	err := os.WriteFile(filename, []byte(`package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+	Version: "1.0",
+	Use:     "example_program",
+	Short:   "example_program - test fixture to check that deadcode elimination is allowed",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("hello world")
+	},
+	Aliases: []string{"alias1", "alias2"},
+	Example: "stringer --help",
+}
+
+func main() {
+	if err := rootCmd.Execute(); err != nil {
+		fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
+		os.Exit(1)
+	}
+}
+`), 0600)
+	if err != nil {
+		t.Fatalf("could not write test program: %v", err)
+	}
+	buf, err := exec.Command("go", "build", filename).CombinedOutput()
+	if err != nil {
+		t.Fatalf("could not compile test program: %s", string(buf))
+	}
+	defer os.Remove(progname)
+	buf, err = exec.Command("go", "tool", "nm", progname).CombinedOutput()
+	if err != nil {
+		t.Fatalf("could not run go tool nm: %v", err)
+	}
+	if strings.Contains(string(buf), "MethodByName") {
+		t.Error("compiled programs contains MethodByName symbol")
+	}
+}
diff --git a/command_test.go b/command_test.go
index 837b6b3..0b0d6c6 100644
--- a/command_test.go
+++ b/command_test.go
@@ -1018,6 +1018,37 @@ func TestSetHelpCommand(t *testing.T) {
 	}
 }
 
+func TestSetHelpTemplate(t *testing.T) {
+	rootCmd := &Command{Use: "root", Run: emptyRun}
+	childCmd := &Command{Use: "child", Run: emptyRun}
+	rootCmd.AddCommand(childCmd)
+
+	rootCmd.SetHelpTemplate("WORKS {{.UseLine}}")
+
+	// Call the help on the root command and check the new template is used
+	got, err := executeCommand(rootCmd, "--help")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	expected := "WORKS " + rootCmd.UseLine()
+	if got != expected {
+		t.Errorf("Expected %q, got %q", expected, got)
+	}
+
+	// Call the help on the child command and check
+	// the new template is inherited from the parent
+	got, err = executeCommand(rootCmd, "child", "--help")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	expected = "WORKS " + childCmd.UseLine()
+	if got != expected {
+		t.Errorf("Expected %q, got %q", expected, got)
+	}
+}
+
 func TestHelpFlagExecuted(t *testing.T) {
 	rootCmd := &Command{Use: "root", Long: "Long description", Run: emptyRun}
 
@@ -1083,6 +1114,33 @@ func TestHelpExecutedOnNonRunnableChild(t *testing.T) {
 	checkStringContains(t, output, childCmd.Long)
 }
 
+func TestSetUsageTemplate(t *testing.T) {
+	rootCmd := &Command{Use: "root", Run: emptyRun}
+	childCmd := &Command{Use: "child", Run: emptyRun}
+	rootCmd.AddCommand(childCmd)
+
+	rootCmd.SetUsageTemplate("WORKS {{.UseLine}}")
+
+	// Trigger the usage on the root command and check the new template is used
+	got, err := executeCommand(rootCmd, "--invalid")
+	if err == nil {
+		t.Errorf("Expected error but did not get one")
+	}
+
+	expected := "WORKS " + rootCmd.UseLine()
+	checkStringContains(t, got, expected)
+
+	// Trigger the usage on the child command and check
+	// the new template is inherited from the parent
+	got, err = executeCommand(rootCmd, "child", "--invalid")
+	if err == nil {
+		t.Errorf("Expected error but did not get one")
+	}
+
+	expected = "WORKS " + childCmd.UseLine()
+	checkStringContains(t, got, expected)
+}
+
 func TestVersionFlagExecuted(t *testing.T) {
 	rootCmd := &Command{Use: "root", Version: "1.0.0", Run: emptyRun}