Skip to content

Commit 61fa353

Browse files
authored
ffcli.DefaultUsageFunc: Support flag help placeholders (#106)
(This is an arguably uncontroversial part of the proposal in #105. Its's a new feature, not a significant change in behavior.) Currently, ffcli.DefaultUsageFunc prints "..." for any flag that does not have a default value specified. This produces less-than-effective help from DefaultUsageFunc. This change retains the behavior of printing the default value as-is, but if a default value is not provided, it allows users to provide placeholder text by wrapping a word inside the help text for a flag in backticks. For example, given the following: fset.String("c", "" /* default */, "path to `config` file") We'll get: -c config path to config file This matches the behavior of FlagSet.PrintDefaults, and indeed it relies on the same flag.UnquoteUsage machinery for this. This also has the nice side-effect of making a reasonable guess at an alternative placeholder text instead of "...". For example: fset.Int("n", "" /* default */, "number of items") // Before: -n ... number of items // Now: -n int number of items Note that as implemented right now, the user supplied placeholder will be used only if a non-zero default value was not supplied. This was an attempt to retain as much of the existing behavior. The proposal in #105, if you're open to it, would change more of the output.
1 parent fe611a8 commit 61fa353

2 files changed

Lines changed: 86 additions & 9 deletions

File tree

ffcli/command.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,33 @@ func DefaultUsageFunc(c *Command) string {
231231
space = "="
232232
}
233233

234-
def := f.DefValue
235-
if def == "" {
234+
// If the help text contains backticks,
235+
// e.g. "foo `bar` baz"`, we'll get:
236+
//
237+
// argname = "bar"
238+
// usage = "foo bar baz"
239+
//
240+
// Otherwise, it's an educated guess for a placeholder,
241+
// or an empty string if one couldn't be determined.
242+
argname, usage := flag.UnquoteUsage(f)
243+
244+
// For the argument name printed in the help,
245+
// the order of preference is:
246+
//
247+
// 1. the default value
248+
// 2. the back-quoted name from the help text
249+
// 3. the '...' placeholder
250+
var def string
251+
switch {
252+
case f.DefValue != "":
253+
def = f.DefValue
254+
case argname != "":
255+
def = argname
256+
default:
236257
def = "..."
237258
}
238259

239-
fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, f.Usage)
260+
fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, usage)
240261
})
241262
tw.Flush()
242263
fmt.Fprintf(&b, "\n")

ffcli/command_test.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,62 @@ func TestIssue57(t *testing.T) {
439439
}
440440
}
441441

442+
func TestDefaultUsageFuncFlagHelp(t *testing.T) {
443+
t.Parallel()
444+
445+
for _, testcase := range []struct {
446+
name string // name of test case
447+
def string // default value, if any
448+
help string // help text for flag
449+
want string // expected usage text
450+
}{
451+
{
452+
name: "plain text",
453+
help: "does stuff",
454+
want: "-x string does stuff",
455+
},
456+
{
457+
name: "placeholder",
458+
help: "reads from `file` instead of stdout",
459+
want: "-x file reads from file instead of stdout",
460+
},
461+
{
462+
name: "default",
463+
def: "www",
464+
help: "path to output directory",
465+
want: "-x www path to output directory",
466+
},
467+
{
468+
name: "default with placeholder",
469+
def: "www",
470+
help: "path to output `directory`",
471+
want: "-x www path to output directory",
472+
},
473+
} {
474+
testcase := testcase
475+
t.Run(testcase.name, func(t *testing.T) {
476+
t.Parallel()
477+
478+
fset := flag.NewFlagSet(t.Name(), flag.ContinueOnError)
479+
fset.String("x", testcase.def, testcase.help)
480+
481+
usage := ffcli.DefaultUsageFunc(&ffcli.Command{
482+
FlagSet: fset,
483+
})
484+
485+
// Discard everything before the FLAGS section.
486+
_, flagUsage, ok := strings.Cut(usage, "\nFLAGS\n")
487+
if !ok {
488+
t.Fatalf("FLAGS section not found in:\n%s", usage)
489+
}
490+
491+
assertMultilineString(t,
492+
strings.TrimSpace(testcase.want),
493+
strings.TrimSpace(flagUsage))
494+
})
495+
}
496+
}
497+
442498
func ExampleCommand_Parse_then_Run() {
443499
// Assume our CLI will use some client that requires a token.
444500
type FooClient struct {
@@ -543,10 +599,10 @@ USAGE
543599
Some long help.
544600
545601
FLAGS
546-
-b=false bool
547-
-d 0s time.Duration
548-
-f 0 float64
549-
-i 0 int
550-
-s ... string
551-
-x ... collection of strings (repeatable)
602+
-b=false bool
603+
-d 0s time.Duration
604+
-f 0 float64
605+
-i 0 int
606+
-s string string
607+
-x ... collection of strings (repeatable)
552608
`) + "\n\n"

0 commit comments

Comments
 (0)