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
This commit is contained in:
Dan Buch 2016-05-02 13:05:21 -04:00
parent d69b4400b5
commit 22773b14c1
No known key found for this signature in database
GPG Key ID: FAEF12936DD3E3EC
4 changed files with 74 additions and 42 deletions

View File

@ -3,6 +3,12 @@
**ATTN**: This project uses [semantic versioning](http://semver.org/). **ATTN**: This project uses [semantic versioning](http://semver.org/).
## [Unreleased] ## [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 ## [1.16.0] - 2016-05-02
### Added ### Added

69
flag.go
View File

@ -31,6 +31,8 @@ var HelpFlag = BoolFlag{
Usage: "show help", Usage: "show help",
} }
var DefaultFlagStringFunc FlagStringFunc = flagStringer
// Flag is a common interface related to parsing flags in cli. // Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that // For more advanced flag parsing techniques, it is recommended that
// this interface be implemented. // 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 // help text to the user (uses the String() method of the generic flag to show
// the value) // the value)
func (f GenericFlag) String() string { func (f GenericFlag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name, placeholder), f.FormatValueHelp(), usage))
} }
func (f GenericFlag) FormatValueHelp() string { func (f GenericFlag) FormatValueHelp() string {
@ -146,10 +147,7 @@ type StringSliceFlag struct {
// String returns the usage // String returns the usage
func (f StringSliceFlag) String() string { func (f StringSliceFlag) String() string {
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") return DefaultFlagStringFunc(f)
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))
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -181,6 +179,12 @@ func (f StringSliceFlag) GetName() string {
return f.Name 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 // StringSlice is an opaque type for []int to satisfy flag.Value
type IntSlice []int type IntSlice []int
@ -217,10 +221,7 @@ type IntSliceFlag struct {
// String returns the usage // String returns the usage
func (f IntSliceFlag) String() string { func (f IntSliceFlag) String() string {
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") return DefaultFlagStringFunc(f)
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))
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -255,6 +256,12 @@ func (f IntSliceFlag) GetName() string {
return f.Name 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 // BoolFlag is a switch that defaults to false
type BoolFlag struct { type BoolFlag struct {
Name string Name string
@ -266,8 +273,7 @@ type BoolFlag struct {
// String returns a readable representation of this value (for usage defaults) // String returns a readable representation of this value (for usage defaults)
func (f BoolFlag) String() string { func (f BoolFlag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name, placeholder), usage))
} }
// Apply populates the flag given the flag set and environment // 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) // String returns a readable representation of this value (for usage defaults)
func (f BoolTFlag) String() string { func (f BoolTFlag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name, placeholder), usage))
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -356,8 +361,7 @@ type StringFlag struct {
// String returns the usage // String returns the usage
func (f StringFlag) String() string { func (f StringFlag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name, placeholder), f.FormatValueHelp(), usage))
} }
func (f StringFlag) FormatValueHelp() string { func (f StringFlag) FormatValueHelp() string {
@ -406,8 +410,7 @@ type IntFlag struct {
// String returns the usage // String returns the usage
func (f IntFlag) String() string { func (f IntFlag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage))
} }
// Apply populates the flag given the flag set and environment // 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) // String returns a readable representation of this value (for usage defaults)
func (f DurationFlag) String() string { func (f DurationFlag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage))
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -496,8 +498,7 @@ type Float64Flag struct {
// String returns the usage // String returns the usage
func (f Float64Flag) String() string { func (f Float64Flag) String() string {
placeholder, usage := unquoteUsage(f.Usage) return DefaultFlagStringFunc(f)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name, placeholder), f.Value, usage))
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -595,3 +596,27 @@ func withEnvHint(envVar, str string) string {
} }
return str + envText 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))
}

View File

@ -7,6 +7,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time"
) )
var boolFlagTests = []struct { var boolFlagTests = []struct {
@ -18,13 +19,12 @@ var boolFlagTests = []struct {
} }
func TestBoolFlagHelpOutput(t *testing.T) { func TestBoolFlagHelpOutput(t *testing.T) {
for _, test := range boolFlagTests { for _, test := range boolFlagTests {
flag := BoolFlag{Name: test.name} flag := BoolFlag{Name: test.name}
output := flag.String() output := flag.String()
if output != test.expected { 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 value string
expected string expected string
}{ }{
{"help", "", "", "--help \t"}, {"help", "", "", "--help\t"},
{"h", "", "", "-h \t"}, {"h", "", "", "-h\t"},
{"h", "", "", "-h \t"}, {"h", "", "", "-h\t"},
{"test", "", "Something", "--test \"Something\"\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) { func TestStringFlagHelpOutput(t *testing.T) {
@ -49,7 +49,7 @@ func TestStringFlagHelpOutput(t *testing.T) {
output := flag.String() output := flag.String()
if output != test.expected { 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 name string
expected string expected string
}{ }{
{"help", "--help \"0\"\t"}, {"help", "--help 0\t"},
{"h", "-h \"0\"\t"}, {"h", "-h 0\t"},
} }
func TestIntFlagHelpOutput(t *testing.T) { func TestIntFlagHelpOutput(t *testing.T) {
@ -168,18 +168,17 @@ var durationFlagTests = []struct {
name string name string
expected string expected string
}{ }{
{"help", "--help \"0\"\t"}, {"help", "--help 1s\t"},
{"h", "-h \"0\"\t"}, {"h", "-h 1s\t"},
} }
func TestDurationFlagHelpOutput(t *testing.T) { func TestDurationFlagHelpOutput(t *testing.T) {
for _, test := range durationFlagTests { for _, test := range durationFlagTests {
flag := DurationFlag{Name: test.name} flag := DurationFlag{Name: test.name, Value: 1 * time.Second}
output := flag.String() output := flag.String()
if output != test.expected { 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 name string
expected string expected string
}{ }{
{"help", "--help \"0\"\t"}, {"help", "--help 0.1\t"},
{"h", "-h \"0\"\t"}, {"h", "-h 0.1\t"},
} }
func TestFloat64FlagHelpOutput(t *testing.T) { func TestFloat64FlagHelpOutput(t *testing.T) {
for _, test := range float64FlagTests { for _, test := range float64FlagTests {
flag := Float64Flag{Name: test.name} flag := Float64Flag{Name: test.name, Value: float64(0.1)}
output := flag.String() output := flag.String()
if output != test.expected { 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) { func TestGenericFlagHelpOutput(t *testing.T) {
for _, test := range genericFlagTests { for _, test := range genericFlagTests {
flag := GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"} flag := GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"}
output := flag.String() output := flag.String()

View File

@ -22,3 +22,7 @@ type CommandNotFoundFunc func(*Context, string)
// original error messages. If this function is not set, the "Incorrect usage" // original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted. // is displayed and the execution is interrupted.
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error 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