From 22773b14c1d7ddda94888c7797405237f547f1db Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 2 May 2016 13:05:21 -0400 Subject: [PATCH] Allow for pluggable flag-level help text formatting by defining `cli.DefaultFlagStringFunc` with a default value that uses `withEnvHint`, conditionally running a given flag's `FormatValueHelp` if present. Closes #257 --- CHANGELOG.md | 6 +++++ flag.go | 69 +++++++++++++++++++++++++++++++++++----------------- flag_test.go | 37 +++++++++++++--------------- funcs.go | 4 +++ 4 files changed, 74 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39581e9..efdc548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ **ATTN**: This project uses [semantic versioning](http://semver.org/). ## [Unreleased] +### Added +- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` + +### Changed +- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer +quoted in help text output. ## [1.16.0] - 2016-05-02 ### Added diff --git a/flag.go b/flag.go index 3c5c1b3..d21e018 100644 --- a/flag.go +++ b/flag.go @@ -31,6 +31,8 @@ var HelpFlag = BoolFlag{ Usage: "show help", } +var DefaultFlagStringFunc FlagStringFunc = flagStringer + // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recommended that // this interface be implemented. @@ -77,8 +79,7 @@ type GenericFlag struct { // help text to the user (uses the String() method of the generic flag to show // the value) func (f GenericFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name, placeholder), f.FormatValueHelp(), usage)) + return DefaultFlagStringFunc(f) } func (f GenericFlag) FormatValueHelp() string { @@ -146,10 +147,7 @@ type StringSliceFlag struct { // String returns the usage func (f StringSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name, placeholder), pref+firstName+" option "+pref+firstName+" option", usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -181,6 +179,12 @@ func (f StringSliceFlag) GetName() string { return f.Name } +func (f StringSliceFlag) FormatValueHelp() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return fmt.Sprintf("[%s%s option %s%s option]", pref, firstName, pref, firstName) +} + // StringSlice is an opaque type for []int to satisfy flag.Value type IntSlice []int @@ -217,10 +221,7 @@ type IntSliceFlag struct { // String returns the usage func (f IntSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name, placeholder), pref+firstName+" option "+pref+firstName+" option", usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -255,6 +256,12 @@ func (f IntSliceFlag) GetName() string { return f.Name } +func (f IntSliceFlag) FormatValueHelp() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return fmt.Sprintf("[%s%s option %s%s option]", pref, firstName, pref, firstName) +} + // BoolFlag is a switch that defaults to false type BoolFlag struct { Name string @@ -266,8 +273,7 @@ type BoolFlag struct { // String returns a readable representation of this value (for usage defaults) func (f BoolFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name, placeholder), usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -311,8 +317,7 @@ type BoolTFlag struct { // String returns a readable representation of this value (for usage defaults) func (f BoolTFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name, placeholder), usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -356,8 +361,7 @@ type StringFlag struct { // String returns the usage func (f StringFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name, placeholder), f.FormatValueHelp(), usage)) + return DefaultFlagStringFunc(f) } func (f StringFlag) FormatValueHelp() string { @@ -406,8 +410,7 @@ type IntFlag struct { // String returns the usage func (f IntFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -451,8 +454,7 @@ type DurationFlag struct { // String returns a readable representation of this value (for usage defaults) func (f DurationFlag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -496,8 +498,7 @@ type Float64Flag struct { // String returns the usage func (f Float64Flag) String() string { - placeholder, usage := unquoteUsage(f.Usage) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage)) + return DefaultFlagStringFunc(f) } // Apply populates the flag given the flag set and environment @@ -595,3 +596,27 @@ func withEnvHint(envVar, str string) string { } return str + envText } + +func flagStringer(f Flag) string { + fv := reflect.ValueOf(f) + placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) + + defaultValueString := "" + if val := fv.FieldByName("Value"); val.IsValid() { + defaultValueString = fmt.Sprintf("%v", val.Interface()) + } + + formatFunc := fv.MethodByName("FormatValueHelp") + if formatFunc.IsValid() { + defaultValueString = formatFunc.Call([]reflect.Value{})[0].String() + } + + if len(defaultValueString) > 0 { + defaultValueString = " " + defaultValueString + } + + return withEnvHint(fv.FieldByName("EnvVar").String(), + fmt.Sprintf("%s%v\t%v", + prefixedNames(fv.FieldByName("Name").String(), placeholder), + defaultValueString, usage)) +} diff --git a/flag_test.go b/flag_test.go index 48d920a..a49cac7 100644 --- a/flag_test.go +++ b/flag_test.go @@ -7,6 +7,7 @@ import ( "runtime" "strings" "testing" + "time" ) var boolFlagTests = []struct { @@ -18,13 +19,12 @@ var boolFlagTests = []struct { } func TestBoolFlagHelpOutput(t *testing.T) { - for _, test := range boolFlagTests { flag := BoolFlag{Name: test.name} output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -35,11 +35,11 @@ var stringFlagTests = []struct { value string expected string }{ - {"help", "", "", "--help \t"}, - {"h", "", "", "-h \t"}, - {"h", "", "", "-h \t"}, + {"help", "", "", "--help\t"}, + {"h", "", "", "-h\t"}, + {"h", "", "", "-h\t"}, {"test", "", "Something", "--test \"Something\"\t"}, - {"config,c", "Load configuration from `FILE`", "", "--config FILE, -c FILE \tLoad configuration from FILE"}, + {"config,c", "Load configuration from `FILE`", "", "--config FILE, -c FILE\tLoad configuration from FILE"}, } func TestStringFlagHelpOutput(t *testing.T) { @@ -49,7 +49,7 @@ func TestStringFlagHelpOutput(t *testing.T) { output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -131,8 +131,8 @@ var intFlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"help", "--help 0\t"}, + {"h", "-h 0\t"}, } func TestIntFlagHelpOutput(t *testing.T) { @@ -168,18 +168,17 @@ var durationFlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"help", "--help 1s\t"}, + {"h", "-h 1s\t"}, } func TestDurationFlagHelpOutput(t *testing.T) { - for _, test := range durationFlagTests { - flag := DurationFlag{Name: test.name} + flag := DurationFlag{Name: test.name, Value: 1 * time.Second} output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -249,18 +248,17 @@ var float64FlagTests = []struct { name string expected string }{ - {"help", "--help \"0\"\t"}, - {"h", "-h \"0\"\t"}, + {"help", "--help 0.1\t"}, + {"h", "-h 0.1\t"}, } func TestFloat64FlagHelpOutput(t *testing.T) { - for _, test := range float64FlagTests { - flag := Float64Flag{Name: test.name} + flag := Float64Flag{Name: test.name, Value: float64(0.1)} output := flag.String() if output != test.expected { - t.Errorf("%s does not match %s", output, test.expected) + t.Errorf("%q does not match %q", output, test.expected) } } } @@ -292,7 +290,6 @@ var genericFlagTests = []struct { } func TestGenericFlagHelpOutput(t *testing.T) { - for _, test := range genericFlagTests { flag := GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"} output := flag.String() diff --git a/funcs.go b/funcs.go index 94640ea..6b2a846 100644 --- a/funcs.go +++ b/funcs.go @@ -22,3 +22,7 @@ type CommandNotFoundFunc func(*Context, string) // original error messages. If this function is not set, the "Incorrect usage" // is displayed and the execution is interrupted. type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error + +// FlagStringFunc is used by the help generation to display a flag, which is +// expected to be a single line. +type FlagStringFunc func(Flag) string