Skip to content

Usage error from Exec #14

@mfridman

Description

@mfridman

Related: #4

From within Exec, there's no way to show usage -- users have to do the awkward variable-capture-plus-closure dance:

var cmd *cli.Command
cmd = &cli.Command{
    Name: "greet",
    Exec: func(ctx context.Context, s *cli.State) error {
        if len(s.Args) == 0 {
            fmt.Fprintln(s.Stderr, cli.DefaultUsage(cmd))
            return errors.New("must supply a name")
        }
        // ...
    },
}

This breaks the clean return &cli.Command{...} pattern.

Options considered

A. Expose Cmd on State + s.UsageErrorf convenience

Two layered changes. Cmd *Command on State is the foundational primitive -- the terminal command, set during Parse. UsageErrorf is convenience built on top.

Requires fixing an artificial limitation: DefaultUsage only works with root today because only root gets state assigned. All commands in the path can share the same *State.

// Foundational: direct access to the command
Exec: func(ctx context.Context, s *cli.State) error {
    fmt.Fprintln(s.Stderr, cli.DefaultUsage(s.Cmd))
    return errors.New("must supply a name")
}

// Convenience: one-liner for the common case
Exec: func(ctx context.Context, s *cli.State) error {
    if len(s.Args) == 0 {
        return s.UsageErrorf("must supply a name")
    }
    // ...
}

No new types, no framework interception. Users pick the level of control they need.

B. Special error type handled by Run/ParseAndRun

Returns a *UsageError wrapping the real error. Run detects it, prints usage to stderr, returns the unwrapped error.

Exec: func(ctx context.Context, s *cli.State) error {
    if len(s.Args) == 0 {
        return cli.UsageErrorf(s, "must supply a name")
    }
    // ...
}

Clean separation (no side effects at the call site), but adds a new exported error type and framework interception logic in Run.

C. Expose s.Usage() string and let users format

Most flexible, least magic. But two lines instead of one, and no single "one way to do it" -- users will format differently or mix up stdout/stderr.

Exec: func(ctx context.Context, s *cli.State) error {
    if len(s.Args) == 0 {
        fmt.Fprintln(s.Stderr, s.Usage())
        return errors.New("must supply a name")
    }
    // ...
}

D. s.PrintUsage() plus normal error return

Like C but hides the fmt.Fprintln boilerplate. Still two lines and two concepts.

Exec: func(ctx context.Context, s *cli.State) error {
    if len(s.Args) == 0 {
        s.PrintUsage()
        return errors.New("must supply a name")
    }
    // ...
}

E. Only UsageErrorf without exposing Cmd

Solves the common case but no escape hatch for less common needs (inspecting command name, flags, building custom usage).

Exec: func(ctx context.Context, s *cli.State) error {
    if len(s.Args) == 0 {
        return s.UsageErrorf("must supply a name")
    }
    // ...
}

Leaning toward A -- Cmd on State is generally useful beyond just usage, and UsageErrorf gives a clean one-liner for the common case.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions