Repository: alecthomas/kong
Pull request #61 (2020-02-21 07:13:08)

add HelpValueFormatter as discussed in #60
As discussed in #60 I made the following changes: 

- add `HelpValueFormatter` (with test)
- remove env interpolation

alecthomas: Nice, this is a good change I think!

Patch:
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, 
-
-
 <a id="markdown-command-handling" name="command-handling"></a>
 ## 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.
 
 <a id="markdown-bind---bind-values-for-callback-hooks-and-run-methods" name="bind---bind-values-for-callback-hooks-and-run-methods"></a>
 ### `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/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/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 {


Test patch:
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/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"`