Skip to content

Commit

Permalink
ffcli.DefaultUsageFunc: Support flag help placeholders (#106)
Browse files Browse the repository at this point in the history
(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.
  • Loading branch information
abhinav authored May 9, 2023
1 parent fe611a8 commit 61fa353
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 9 deletions.
27 changes: 24 additions & 3 deletions ffcli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
68 changes: 62 additions & 6 deletions ffcli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"

0 comments on commit 61fa353

Please sign in to comment.