Skip to content

Refactor help API#19

Draft
mfridman wants to merge 2 commits intomainfrom
mf/usage-api
Draft

Refactor help API#19
mfridman wants to merge 2 commits intomainfrom
mf/usage-api

Conversation

@mfridman
Copy link
Copy Markdown
Collaborator

@mfridman mfridman commented Apr 26, 2026

Reshapes help customization and usage-error handling from the API that exists on main today. The current API has Command.UsageFunc func(*Command) string for replacing help wholesale, DefaultUsage(*Command) string for the built-in formatter, and FlagOption / Command.FlagOptions for flag metadata. This PR replaces the string-based help hook with a structured help document, adds a small optional usage package for composing that document, exposes the resolved command through State.Cmd, and adds an opt-in cli.UsageErrorf path for bad invocations from Exec.

The intended default path stays small: most programs still define a command tree and call ParseAndRun. --help still works automatically. The new pieces are for the cases where the built-in help needs light customization, or where an Exec function needs to say "the command was selected, but the provided args or flag combination are invalid."

Closes #4, #14, #15.

Why this refactor

On main, UsageFunc is all-or-nothing: it returns a final string. That works for replacing the entire help output, but it is awkward for the common case of keeping the default help and adding one section, such as Examples. Callers either need to rebuild the default formatter or concatenate strings around DefaultUsage. That also makes DefaultUsage and UsageFunc tightly coupled: DefaultUsage dispatches to UsageFunc, so calling DefaultUsage from inside UsageFunc recurses. Passing the built-in help document into the new hook makes composition the default and makes that recursion impossible.

Usage errors have a similar ergonomics problem on main: parse errors happen before Exec, but many invalid invocations are only obvious inside Exec, after flags and positional args are available. Without a first-class usage error, callers have to manually print usage at each call site or treat bad invocation errors the same as runtime failures. cli.UsageErrorf gives command code a small convention: return it for invalid command-line usage, and Run prints the resolved command help before returning the underlying error.

State.Cmd is the primitive that makes this work. It gives Exec access to the resolved command, so code can inspect the command path with s.Cmd.Path() or render help for the selected command with cli.Help(s.Cmd).

FlagOption is renamed to FlagConfig because it is a configuration record attached to a flag, not an option in the functional-options sense.

New API shape

  • cli.Help(*Command) usage.Help builds the help document for the resolved command.
  • Command.Help func(*Command, usage.Help) usage.Help customizes help by receiving the built-in document and returning the final document.
  • usage provides composable blocks: Text, Lines, List, Flags, and Commands.
  • State.Cmd exposes the resolved command inside Exec.
  • cli.UsageErrorf marks invalid command-line usage from Exec; Run prints command help to stderr and returns the underlying error.
  • FlagConfig and Command.FlagConfigs replace FlagOption and Command.FlagOptions.

Breaking changes

  • Command.UsageFunc is removed. Use Command.Help.
  • DefaultUsage and Usage are removed. Use cli.Help(cmd).String() or render the returned usage.Help.
  • Help customization now composes usage.Help documents instead of returning a fully formatted string.
  • FlagOption is renamed to FlagConfig; Command.FlagOptions is renamed to Command.FlagConfigs.

Migration examples

// main
cmd.UsageFunc = func(c *cli.Command) string {
    return "custom help"
}

// this PR
cmd.Help = func(c *cli.Command, h usage.Help) usage.Help {
    return usage.Help{usage.Text("custom help")}
}
// main: adding examples requires owning or concatenating help text
cmd.UsageFunc = func(c *cli.Command) string {
    return "...full help text..."
}

// this PR: keep defaults and append a section
cmd.Help = func(c *cli.Command, h usage.Help) usage.Help {
    return append(h, usage.Lines("Examples:", "greet margo"))
}
// main
FlagOptions: []cli.FlagOption{{Name: "verbose", Short: "v"}},

// this PR
FlagConfigs: []cli.FlagConfig{{Name: "verbose", Short: "v"}},
// this PR: invalid invocation from Exec
Exec: func(ctx context.Context, s *cli.State) error {
    if len(s.Args) == 0 {
        return cli.UsageErrorf("must supply a name")
    }
    return nil
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add ability to get command information from *State

1 participant