Allow linker to perform deadcode elimination for program using Cobra (#1956)

* Restructure code to let linker perform deadcode elimination step

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

Co-authored-by: Marc Khouzam <marc.khouzam@gmail.com>
This commit is contained in:
Alessandro Arzilli 2025-01-27 15:43:43 +01:00 committed by GitHub
parent 09d5664f34
commit 611e16c322
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 365 additions and 61 deletions

View file

@ -176,12 +176,16 @@ func rpad(s string, padding int) string {
return fmt.Sprintf(formattedString, s) return fmt.Sprintf(formattedString, s)
} }
// tmpl executes the given template text on data, writing the result to w. func tmpl(text string) *tmplFunc {
func tmpl(w io.Writer, text string, data interface{}) error { return &tmplFunc{
tmpl: text,
fn: func(w io.Writer, data interface{}) error {
t := template.New("top") t := template.New("top")
t.Funcs(templateFuncs) t.Funcs(templateFuncs)
template.Must(t.Parse(text)) template.Must(t.Parse(text))
return t.Execute(w, data) return t.Execute(w, data)
},
}
} }
// ld compares two strings and returns the levenshtein distance between them. // ld compares two strings and returns the levenshtein distance between them.

View file

@ -15,6 +15,11 @@
package cobra package cobra
import ( import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing" "testing"
"text/template" "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")
}
}

View file

@ -32,6 +32,9 @@ import (
const ( const (
FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra"
CommandDisplayNameAnnotation = "cobra_annotation_command_display_name" CommandDisplayNameAnnotation = "cobra_annotation_command_display_name"
helpFlagName = "help"
helpCommandName = "help"
) )
// FParseErrWhitelist configures Flag parse errors to be ignored // FParseErrWhitelist configures Flag parse errors to be ignored
@ -167,12 +170,12 @@ type Command struct {
// usageFunc is usage func defined by user. // usageFunc is usage func defined by user.
usageFunc func(*Command) error usageFunc func(*Command) error
// usageTemplate is usage template defined by user. // 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 // flagErrorFunc is func defined by user and it's called when the parsing of
// flags returns an error. // flags returns an error.
flagErrorFunc func(*Command, error) error flagErrorFunc func(*Command, error) error
// helpTemplate is help template defined by user. // helpTemplate is help template defined by user.
helpTemplate string helpTemplate *tmplFunc
// helpFunc is help func defined by user. // helpFunc is help func defined by user.
helpFunc func(*Command, []string) helpFunc func(*Command, []string)
// helpCommand is command with usage 'help'. If it's not defined by user, // helpCommand is command with usage 'help'. If it's not defined by user,
@ -185,7 +188,7 @@ type Command struct {
completionCommandGroupID string completionCommandGroupID string
// versionTemplate is the version template defined by user. // versionTemplate is the version template defined by user.
versionTemplate string versionTemplate *tmplFunc
// errPrefix is the error message prefix defined by user. // errPrefix is the error message prefix defined by user.
errPrefix string errPrefix string
@ -312,7 +315,7 @@ func (c *Command) SetUsageFunc(f func(*Command) error) {
// SetUsageTemplate sets usage template. Can be defined by Application. // SetUsageTemplate sets usage template. Can be defined by Application.
func (c *Command) SetUsageTemplate(s string) { func (c *Command) SetUsageTemplate(s string) {
c.usageTemplate = s c.usageTemplate = tmpl(s)
} }
// SetFlagErrorFunc sets a function to generate an error when flag parsing // 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. // SetHelpTemplate sets help template to be used. Application can use it to set custom template.
func (c *Command) SetHelpTemplate(s string) { 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. // SetVersionTemplate sets version template to be used. Application can use it to set custom template.
func (c *Command) SetVersionTemplate(s string) { 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. // 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 { return func(c *Command) error {
c.mergePersistentFlags() c.mergePersistentFlags()
err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c) fn := c.getUsageTemplateFunc()
err := fn(c.OutOrStderr(), c)
if err != nil { if err != nil {
c.PrintErrln(err) 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. // Usage puts out the usage for the command.
// Used when a user provides invalid input. // Used when a user provides invalid input.
// Can be defined by user by overriding UsageFunc. // 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) { return func(c *Command, a []string) {
c.mergePersistentFlags() c.mergePersistentFlags()
fn := c.getHelpTemplateFunc()
// The help should be sent to stdout // The help should be sent to stdout
// See https://github.com/spf13/cobra/issues/1002 // See https://github.com/spf13/cobra/issues/1002
err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c) err := fn(c.OutOrStdout(), c)
if err != nil { if err != nil {
c.PrintErrln(err) 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. // Help puts out the help for the command.
// Used when a user calls help [command]. // Used when a user calls help [command].
// Can be defined by user by overriding HelpFunc. // Can be defined by user by overriding HelpFunc.
@ -543,71 +575,55 @@ func (c *Command) NamePadding() int {
} }
// UsageTemplate returns usage template for the command. // UsageTemplate returns usage template for the command.
// This function is kept for backwards-compatibility reasons.
func (c *Command) UsageTemplate() string { func (c *Command) UsageTemplate() string {
if c.usageTemplate != "" { if c.usageTemplate != nil {
return c.usageTemplate return c.usageTemplate.tmpl
} }
if c.HasParent() { if c.HasParent() {
return c.parent.UsageTemplate() return c.parent.UsageTemplate()
} }
return `Usage:{{if .Runnable}} return defaultUsageTemplate
{{.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}}
`
} }
// HelpTemplate return help template for the command. // HelpTemplate return help template for the command.
// This function is kept for backwards-compatibility reasons.
func (c *Command) HelpTemplate() string { func (c *Command) HelpTemplate() string {
if c.helpTemplate != "" { if c.helpTemplate != nil {
return c.helpTemplate return c.helpTemplate.tmpl
} }
if c.HasParent() { if c.HasParent() {
return c.parent.HelpTemplate() return c.parent.HelpTemplate()
} }
return `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} return defaultHelpTemplate
{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
} }
// VersionTemplate return version template for the command. // VersionTemplate return version template for the command.
// This function is kept for backwards-compatibility reasons.
func (c *Command) VersionTemplate() string { func (c *Command) VersionTemplate() string {
if c.versionTemplate != "" { if c.versionTemplate != nil {
return c.versionTemplate return c.versionTemplate.tmpl
} }
if c.HasParent() { if c.HasParent() {
return c.parent.VersionTemplate() 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 // 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. // If help is called, regardless of other flags, return we want help.
// Also say we need help if the command isn't runnable. // 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 { if err != nil {
// should be impossible to get here as we always declare a help // should be impossible to get here as we always declare a help
// flag in InitDefaultHelpFlag() // flag in InitDefaultHelpFlag()
@ -914,7 +930,8 @@ func (c *Command) execute(a []string) (err error) {
return err return err
} }
if versionVal { if versionVal {
err := tmpl(c.OutOrStdout(), c.VersionTemplate(), c) fn := c.getVersionTemplateFunc()
err := fn(c.OutOrStdout(), c)
if err != nil { if err != nil {
c.Println(err) c.Println(err)
} }
@ -1190,7 +1207,7 @@ func (c *Command) checkCommandGroups() {
// If c already has help flag, it will do nothing. // If c already has help flag, it will do nothing.
func (c *Command) InitDefaultHelpFlag() { func (c *Command) InitDefaultHelpFlag() {
c.mergePersistentFlags() c.mergePersistentFlags()
if c.Flags().Lookup("help") == nil { if c.Flags().Lookup(helpFlagName) == nil {
usage := "help for " usage := "help for "
name := c.DisplayName() name := c.DisplayName()
if name == "" { if name == "" {
@ -1198,8 +1215,8 @@ func (c *Command) InitDefaultHelpFlag() {
} else { } else {
usage += name usage += name
} }
c.Flags().BoolP("help", "h", false, usage) c.Flags().BoolP(helpFlagName, "h", false, usage)
_ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"}) _ = c.Flags().SetAnnotation(helpFlagName, FlagSetByCobraAnnotation, []string{"true"})
} }
} }
@ -1899,3 +1916,141 @@ func commandNameMatches(s string, t string) bool {
return s == t 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
}

View file

@ -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) { func TestHelpFlagExecuted(t *testing.T) {
rootCmd := &Command{Use: "root", Long: "Long description", Run: emptyRun} rootCmd := &Command{Use: "root", Long: "Long description", Run: emptyRun}
@ -1083,6 +1114,33 @@ func TestHelpExecutedOnNonRunnableChild(t *testing.T) {
checkStringContains(t, output, childCmd.Long) 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) { func TestVersionFlagExecuted(t *testing.T) {
rootCmd := &Command{Use: "root", Version: "1.0.0", Run: emptyRun} rootCmd := &Command{Use: "root", Version: "1.0.0", Run: emptyRun}

View file

@ -555,7 +555,7 @@ func helpOrVersionFlagPresent(cmd *Command) bool {
len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed { len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed {
return true 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 { len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed {
return true return true
} }

View file

@ -554,6 +554,9 @@ cmd.SetHelpTemplate(s string)
The latter two will also apply to any children commands. 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 ## Usage Message
When the user provides an invalid flag or invalid command, Cobra responds by 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 ### Defining your own usage
You can provide your own usage function or template for Cobra to use. You can provide your own usage function or template for Cobra to use.
Like help, the function and template are overridable through public methods: 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) 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 ## Version Flag
Cobra adds a top-level '--version' flag if the Version field is set on the root command. 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 the version template. The template can be customized using the
`cmd.SetVersionTemplate(s string)` function. `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 ## Error Message Prefix
Cobra prints an error message when receiving a non-nil error value. Cobra prints an error message when receiving a non-nil error value.