Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 70 additions & 31 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"regexp"
"strings"
"text/template"
)

var (
Expand All @@ -28,17 +27,20 @@ type Application struct {
Name string
Help string

author string
version string
errorWriter io.Writer // Destination for errors.
usageWriter io.Writer // Destination for usage
usageTemplate string
usageFuncs template.FuncMap
validator ApplicationValidator
terminate func(status int) // See Terminate()
noInterspersed bool // can flags be interspersed with args (or must they come first)
defaultEnvars bool
completion bool
author string
version string
errorWriter io.Writer // Destination for errors.
usageWriter io.Writer // Destination for usage
hiddenHelpWriter io.Writer // Desitination for hidden help commands.
usageTemplate string
usageFuncs map[string]interface{}
templateRenderer func(a *Application, context *ParseContext, indent int, tmpl string) error
usageRenderer UsageRenderer
validator ApplicationValidator
terminate func(status int) // See Terminate()
noInterspersed bool // can flags be interspersed with args (or must they come first)
defaultEnvars bool
completion bool

// Help flag. Exposed for user customisation.
HelpFlag *FlagClause
Expand All @@ -51,12 +53,12 @@ type Application struct {
// New creates a new Kingpin application instance.
func New(name, help string) *Application {
a := &Application{
Name: name,
Help: help,
errorWriter: os.Stderr, // Left for backwards compatibility purposes.
usageWriter: os.Stderr,
usageTemplate: DefaultUsageTemplate,
terminate: os.Exit,
Name: name,
Help: help,
errorWriter: os.Stderr, // Left for backwards compatibility purposes.
usageWriter: os.Stderr,
hiddenHelpWriter: os.Stdout,
terminate: os.Exit,
}
a.flagGroup = newFlagGroup()
a.argGroup = newArgGroup()
Expand All @@ -73,45 +75,57 @@ func New(name, help string) *Application {
return a
}

// renderHiddenFlag renders usage for hidden help flags (--help-long, --help-man, etc.).
// A custom UsageRenderer does NOT override these — they are distinct output formats
// (man pages, completion scripts) that should always produce their advertised output.
// UsageFuncs overrides ARE applied, since they provide template helper functions that
// may be needed by the template path.
func (a *Application) renderHiddenFlag(c *ParseContext, renderer UsageRenderer, tmpl string) error {
if a.usageFuncs != nil && a.templateRenderer != nil {
return a.templateRenderer(a, c, 2, tmpl)
}
return a.usageForContextWithUsageRenderer(c, 2, renderer)
}

func (a *Application) generateLongHelp(c *ParseContext) error {
a.Writer(os.Stdout)
if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil {
a.Writer(a.hiddenHelpWriter)
if err := a.renderHiddenFlag(c, RenderLongHelp, LongHelpTemplate); err != nil {
return err
}
a.terminate(0)
return nil
}

func (a *Application) generateManPage(c *ParseContext) error {
a.Writer(os.Stdout)
if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil {
a.Writer(a.hiddenHelpWriter)
if err := a.renderHiddenFlag(c, RenderManPage, ManPageTemplate); err != nil {
return err
}
a.terminate(0)
return nil
}

func (a *Application) generateBashCompletionScript(c *ParseContext) error {
a.Writer(os.Stdout)
if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil {
a.Writer(a.hiddenHelpWriter)
if err := a.renderHiddenFlag(c, RenderBashCompletion, BashCompletionTemplate); err != nil {
return err
}
a.terminate(0)
return nil
}

func (a *Application) generateZSHCompletionScript(c *ParseContext) error {
a.Writer(os.Stdout)
if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil {
a.Writer(a.hiddenHelpWriter)
if err := a.renderHiddenFlag(c, RenderZshCompletion, ZshCompletionTemplate); err != nil {
return err
}
a.terminate(0)
return nil
}

func (a *Application) generateFishCompletionScript(c *ParseContext) error {
a.Writer(os.Stdout)
if err := a.UsageForContextWithTemplate(c, 2, FishCompletionTemplate); err != nil {
a.Writer(a.hiddenHelpWriter)
if err := a.renderHiddenFlag(c, RenderFishCompletion, FishCompletionTemplate); err != nil {
return err
}
a.terminate(0)
Expand Down Expand Up @@ -152,22 +166,47 @@ func (a *Application) ErrorWriter(w io.Writer) *Application {
return a
}

// UsageWriter sets the io.Writer to use for errors.
// UsageWriter sets the io.Writer to use for usage.
func (a *Application) UsageWriter(w io.Writer) *Application {
a.usageWriter = w
return a
}

// HiddenHelpWriter sets the io.Writer to use for usage of hidden help commands.
func (a *Application) HiddenHelpWriter(w io.Writer) *Application {
a.hiddenHelpWriter = w
return a
}

// UsageTemplate specifies the text template to use when displaying usage
// information. The default is UsageTemplate.
//
// Note: calling this method causes text/template to be linked into the binary,
// which prevents dead code elimination of reflect.MethodByName. Programs that
// want smaller binaries should use UsageRenderer instead.
func (a *Application) UsageTemplate(template string) *Application {
a.usageTemplate = template
a.templateRenderer = templateRenderFunc
return a
}

// UsageFuncs adds extra functions that can be used in the usage template.
func (a *Application) UsageFuncs(funcs template.FuncMap) *Application {
// UsageFuncs adds extra functions that can be used in the usage template
//
// Note: calling this method causes text/template to be linked into the binary,
// which prevents dead code elimination of reflect.MethodByName. Programs that
// want smaller binaries should use UsageRenderer instead..
func (a *Application) UsageFuncs(funcs map[string]interface{}) *Application {
a.usageFuncs = funcs
a.templateRenderer = templateRenderFunc
return a
}

// UsageRenderer registers a custom UsageRenderer for the primary --help output.
// It does not affect hidden help flags (--help-long, --help-man, completion scripts),
// which always use their built-in renderers or the template path if UsageFuncs is set.
// For backward compatibility, UsageTemplate takes precedence over UsageRenderer.
func (a *Application) UsageRenderer(fn UsageRenderer) *Application {
a.usageRenderer = fn
return a
}

Expand Down
49 changes: 49 additions & 0 deletions dce_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kingpin

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/require"
)

// TestDeadCodeElimination verifies that programs using kingpin's default
// UsageRenderer do not link in reflect.MethodByName, which is the key
// indicator that dead code elimination is working.
func TestDeadCodeElimination(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("go tool nm not reliable on Windows")
}

dir := t.TempDir()
filename := filepath.Join(dir, "main.go")
err := os.WriteFile(filename, []byte(`package main

import (
"os"

"github.com/alecthomas/kingpin/v2"
)

func main() {
app := kingpin.New("test", "A test app.")
app.UsageRenderer(kingpin.RenderDefault)
app.Flag("verbose", "Enable verbose mode.").Bool()
app.Command("sub", "A subcommand.")
app.Parse(os.Args[1:])
}
`), 0o600)
require.NoError(t, err)

binPath := filepath.Join(dir, "test_binary")
buf, err := exec.Command("go", "build", "-trimpath", "-o", binPath, filename).CombinedOutput()
require.NoError(t, err, "go build failed: %s", buf)

buf, err = exec.Command("go", "tool", "nm", binPath).CombinedOutput()
require.NoError(t, err, "go tool nm failed: %s", buf)

require.NotContains(t, string(buf), "MethodByName", "text/template was not eliminated by dead code elimination")
}
Loading
Loading