From 61fa353023b541c2d34894d540e0d3becbf4b8fe Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 9 May 2023 03:02:14 -0600 Subject: [PATCH] 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. --- ffcli/command.go | 27 +++++++++++++++-- ffcli/command_test.go | 68 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/ffcli/command.go b/ffcli/command.go index ed83d19..5254acc 100644 --- a/ffcli/command.go +++ b/ffcli/command.go @@ -231,12 +231,33 @@ func DefaultUsageFunc(c *Command) string { space = "=" } - def := f.DefValue - if def == "" { + // If the help text contains backticks, + // e.g. "foo `bar` baz"`, we'll get: + // + // argname = "bar" + // usage = "foo bar baz" + // + // Otherwise, it's an educated guess for a placeholder, + // or an empty string if one couldn't be determined. + argname, usage := flag.UnquoteUsage(f) + + // For the argument name printed in the help, + // the order of preference is: + // + // 1. the default value + // 2. the back-quoted name from the help text + // 3. the '...' placeholder + var def string + switch { + case f.DefValue != "": + def = f.DefValue + case argname != "": + def = argname + default: def = "..." } - fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, f.Usage) + fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, usage) }) tw.Flush() fmt.Fprintf(&b, "\n") diff --git a/ffcli/command_test.go b/ffcli/command_test.go index 7656a85..9d9aa90 100644 --- a/ffcli/command_test.go +++ b/ffcli/command_test.go @@ -439,6 +439,62 @@ func TestIssue57(t *testing.T) { } } +func TestDefaultUsageFuncFlagHelp(t *testing.T) { + t.Parallel() + + for _, testcase := range []struct { + name string // name of test case + def string // default value, if any + help string // help text for flag + want string // expected usage text + }{ + { + name: "plain text", + help: "does stuff", + want: "-x string does stuff", + }, + { + name: "placeholder", + help: "reads from `file` instead of stdout", + want: "-x file reads from file instead of stdout", + }, + { + name: "default", + def: "www", + help: "path to output directory", + want: "-x www path to output directory", + }, + { + name: "default with placeholder", + def: "www", + help: "path to output `directory`", + want: "-x www path to output directory", + }, + } { + testcase := testcase + t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + + fset := flag.NewFlagSet(t.Name(), flag.ContinueOnError) + fset.String("x", testcase.def, testcase.help) + + usage := ffcli.DefaultUsageFunc(&ffcli.Command{ + FlagSet: fset, + }) + + // Discard everything before the FLAGS section. + _, flagUsage, ok := strings.Cut(usage, "\nFLAGS\n") + if !ok { + t.Fatalf("FLAGS section not found in:\n%s", usage) + } + + assertMultilineString(t, + strings.TrimSpace(testcase.want), + strings.TrimSpace(flagUsage)) + }) + } +} + func ExampleCommand_Parse_then_Run() { // Assume our CLI will use some client that requires a token. type FooClient struct { @@ -543,10 +599,10 @@ USAGE Some long help. FLAGS - -b=false bool - -d 0s time.Duration - -f 0 float64 - -i 0 int - -s ... string - -x ... collection of strings (repeatable) + -b=false bool + -d 0s time.Duration + -f 0 float64 + -i 0 int + -s string string + -x ... collection of strings (repeatable) `) + "\n\n"