From 7b000bd7759ae608936a140578e2c2310c9f013c Mon Sep 17 00:00:00 2001 From: Rene Zbinden Date: Fri, 21 Feb 2020 10:24:19 +0100 Subject: [PATCH] Add HelpValueFormatter as discussed in #60 (#61) Fixes #60 . --- README.md | 8 ++------ help.go | 41 +++++++++++++++++++++++++++++++---------- help_test.go | 29 +++++++++++++++++++++++++++++ interpolate.go | 11 ----------- kong.go | 30 ++++++++---------------------- kong_test.go | 11 ----------- options.go | 8 ++++++++ 7 files changed, 78 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index a96c059..315c7a7 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ 2. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) 3. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources) 4. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values) - 5. [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) + 5. [`ConfigureHelp(HelpOptions)`, `Help(HelpFunc)` and `HelpFormatter(HelpValueFormatter)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) 6. [`Bind(...)` - bind values for callback hooks and Run() methods](#bind---bind-values-for-callback-hooks-and-run-methods) 7. [Other options](#other-options) @@ -117,10 +117,6 @@ eg. -f, --force Force removal. -r, --recursive Recursively remove files. -For flags with associated environment variables, the variable `${env}` can be -interpolated into the help string. In the absence of this variable in the help, - - ## Command handling @@ -463,7 +459,6 @@ are defined from the value itself: ${default} ${enum} - ${env} eg. @@ -546,6 +541,7 @@ The default help output is usually sufficient, but if not there are two solution 1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details). 2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example. +3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments. ### `Bind(...)` - bind values for callback hooks and Run() methods diff --git a/help.go b/help.go index 8bb67cd..4814b1c 100644 --- a/help.go +++ b/help.go @@ -65,6 +65,25 @@ type HelpIndenter func(prefix string) string // HelpPrinter is used to print context-sensitive help. type HelpPrinter func(options HelpOptions, ctx *Context) error +// HelpValueFormatter is used to format the help text of flags and positional arguments. +type HelpValueFormatter func(value *Value) string + +// DefaultHelpValueFormatter is the default HelpValueFormatter. +func DefaultHelpValueFormatter(value *Value) string { + if value.Tag.Env == "" { + return value.Help + } + suffix := "($" + value.Tag.Env + ")" + switch { + case strings.HasSuffix(value.Help, "."): + return value.Help[:len(value.Help)-1] + " " + suffix + "." + case value.Help == "": + return suffix + default: + return value.Help + " " + suffix + } +} + // DefaultHelpPrinter is the default HelpPrinter. func DefaultHelpPrinter(options HelpOptions, ctx *Context) error { if ctx.Empty() { @@ -214,19 +233,21 @@ func printCommandSummary(w *helpWriter, cmd *Command) { } type helpWriter struct { - indent string - width int - lines *[]string + indent string + width int + lines *[]string + helpFormatter HelpValueFormatter HelpOptions } func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter { lines := []string{} w := &helpWriter{ - indent: "", - width: guessWidth(ctx.Stdout), - lines: &lines, - HelpOptions: options, + indent: "", + width: guessWidth(ctx.Stdout), + lines: &lines, + helpFormatter: ctx.Kong.helpFormatter, + HelpOptions: options, } return w } @@ -240,7 +261,7 @@ func (h *helpWriter) Print(text string) { } func (h *helpWriter) Indent() *helpWriter { - return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions} + return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions, helpFormatter: h.helpFormatter} } func (h *helpWriter) String() string { @@ -268,7 +289,7 @@ func (h *helpWriter) Wrap(text string) { func writePositionals(w *helpWriter, args []*Positional) { rows := [][2]string{} for _, arg := range args { - rows = append(rows, [2]string{arg.Summary(), arg.Help}) + rows = append(rows, [2]string{arg.Summary(), w.helpFormatter(arg)}) } writeTwoColumns(w, rows) } @@ -290,7 +311,7 @@ func writeFlags(w *helpWriter, groups [][]*Flag) { } for _, flag := range group { if !flag.Hidden { - rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help}) + rows = append(rows, [2]string{formatFlag(haveShort, flag), w.helpFormatter(flag.Value)}) } } } diff --git a/help_test.go b/help_test.go index fbe4a47..3f6f368 100644 --- a/help_test.go +++ b/help_test.go @@ -2,6 +2,7 @@ package kong_test import ( "bytes" + "strings" "testing" "github.com/stretchr/testify/require" @@ -219,3 +220,31 @@ Commands: require.Equal(t, expected, w.String()) }) } + +func TestEnvarAutoHelp(t *testing.T) { + var cli struct { + Flag string `env:"FLAG" help:"A flag."` + } + w := &strings.Builder{} + p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {})) + _, err := p.Parse([]string{"--help"}) + require.NoError(t, err) + require.Contains(t, w.String(), "A flag ($FLAG).") +} + +func TestCustomHelpFormatter(t *testing.T) { + var cli struct { + Flag string `env:"FLAG" help:"A flag."` + } + w := &strings.Builder{} + p := mustNew(t, &cli, + kong.Writers(w, w), + kong.Exit(func(int) {}), + kong.HelpFormatter(func(value *kong.Value) string { + return value.Help + }), + ) + _, err := p.Parse([]string{"--help"}) + require.NoError(t, err) + require.Contains(t, w.String(), "A flag.") +} diff --git a/interpolate.go b/interpolate.go index bbfff54..9de1ff7 100644 --- a/interpolate.go +++ b/interpolate.go @@ -7,17 +7,6 @@ import ( var interpolationRegex = regexp.MustCompile(`((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`) -// Returns true if the variable "v" is interpolated in "s". -func interpolationHasVar(s string, v string) bool { - matches := interpolationRegex.FindAllStringSubmatch(s, -1) - for _, match := range matches { - if name := match[2]; name == v { - return true - } - } - return false -} - // Interpolate variables from vars into s for substrings in the form ${var} or ${var=default}. func interpolate(s string, vars map[string]string) (string, error) { out := "" diff --git a/kong.go b/kong.go index 5296ef2..882e4ad 100644 --- a/kong.go +++ b/kong.go @@ -50,6 +50,7 @@ type Kong struct { noDefaultHelp bool usageOnError bool help HelpPrinter + helpFormatter HelpValueFormatter helpOptions HelpOptions helpFlag *Flag vars Vars @@ -63,12 +64,13 @@ type Kong struct { // See the README (https://github.com/alecthomas/kong) for usage instructions. func New(grammar interface{}, options ...Option) (*Kong, error) { k := &Kong{ - Exit: os.Exit, - Stdout: os.Stdout, - Stderr: os.Stderr, - registry: NewRegistry().RegisterDefaults(), - vars: Vars{}, - bindings: bindings{}, + Exit: os.Exit, + Stdout: os.Stdout, + Stderr: os.Stderr, + registry: NewRegistry().RegisterDefaults(), + vars: Vars{}, + bindings: bindings{}, + helpFormatter: DefaultHelpValueFormatter, } options = append(options, Bind(k)) @@ -153,22 +155,6 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { "default": value.Default, "enum": value.Enum, }) - if value.Tag.Env != "" { - vars["env"] = value.Tag.Env - if !interpolationHasVar(value.Help, "env") { - suffix := "($" + value.Tag.Env + ")" - switch { - case strings.HasSuffix(value.Help, "."): - value.Help = value.Help[:len(value.Help)-1] + " " + suffix + "." - - case value.Help == "": - value.Help += suffix - - default: - value.Help += " " + suffix - } - } - } if value.Help, err = interpolate(value.Help, vars); err != nil { return fmt.Errorf("help for %s: %s", value.Summary(), err) } diff --git a/kong_test.go b/kong_test.go index 087252a..b55093a 100644 --- a/kong_test.go +++ b/kong_test.go @@ -750,17 +750,6 @@ func TestEnvarEnumValidated(t *testing.T) { require.EqualError(t, err, "--flag must be one of \"valid\" but got \"invalid\"") } -func TestEnvarAutoHelp(t *testing.T) { - var cli struct { - Flag string `env:"FLAG" help:"A flag."` - } - w := &strings.Builder{} - p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {})) - _, err := p.Parse([]string{"--help"}) - require.NoError(t, err) - require.Contains(t, w.String(), "A flag ($FLAG).") -} - func TestXor(t *testing.T) { var cli struct { Hello bool `xor:"another"` diff --git a/options.go b/options.go index aec7164..1bf5219 100644 --- a/options.go +++ b/options.go @@ -158,6 +158,14 @@ func Help(help HelpPrinter) Option { }) } +// HelpFormatter configures how the help text is formatted. +func HelpFormatter(helpFormatter HelpValueFormatter) Option { + return OptionFunc(func(k *Kong) error { + k.helpFormatter = helpFormatter + return nil + }) +} + // ConfigureHelp sets the HelpOptions to use for printing help. func ConfigureHelp(options HelpOptions) Option { return OptionFunc(func(k *Kong) error {