Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- `flagtype.EnumDefault` constructor for enums with an initial default value
- New optional `usage` package with composable help document blocks: `Document`, `Text`, `Lines`,
`List`, `Flags`, and `Commands`
- `usage.New` and `usage.Help` for building default help documents or rendering command help
- `Cmd` field on `State` for accessing the terminal command selected by parsing
- `UsageErrorf` for opt-in usage errors; `Run` prints command help to stderr before returning the
underlying error

### Changed

- **BREAKING**: Replace `Command.UsageFunc` with `Command.Help`, which returns the full help string
for a command
- **BREAKING**: Rename `Command.ShortHelp` to `Command.Description`
- **BREAKING**: Rename `FlagOption` to `FlagConfig` and `Command.FlagOptions` to
`Command.FlagConfigs`
- Help output keeps the default automatic `--help` behavior while making the richer `usage` package
optional

### Removed

- **BREAKING**: Remove `ErrHelp`; check `errors.Is(err, flag.ErrHelp)` when handling `Parse`
directly
- **BREAKING**: Remove `DefaultUsage` and the top-level `Usage` function; use `usage.Help(cmd)` for
direct help rendering

## [v0.6.0] - 2026-02-18

Expand Down
76 changes: 66 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Requires Go 1.21 or higher.

```go
root := &cli.Command{
Name: "greet",
ShortHelp: "Print a greeting",
Name: "greet",
Description: "Print a greeting",
Exec: func(ctx context.Context, s *cli.State) error {
fmt.Fprintln(s.Stdout, "hello, world!")
return nil
Expand All @@ -41,15 +41,15 @@ resolved command. For applications that need work between parsing and execution,

## Flags

`FlagsFunc` is a convenience for defining flags inline. Use `FlagOptions` to extend the standard
`FlagsFunc` is a convenience for defining flags inline. Use `FlagConfigs` to extend the standard
`flag` package with features like required flag enforcement and short aliases:

```go
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
f.Bool("verbose", false, "enable verbose output")
f.String("output", "", "output file")
}),
FlagOptions: []cli.FlagOption{
FlagConfigs: []cli.FlagConfig{
{Name: "verbose", Short: "v"},
{Name: "output", Short: "o", Required: true},
},
Expand All @@ -74,13 +74,13 @@ Commands can have nested subcommands, each with their own flags and `Exec` funct

```go
root := &cli.Command{
Name: "todo",
Usage: "todo <command> [flags]",
ShortHelp: "A simple CLI for managing your tasks",
Name: "todo",
Usage: "todo <command> [flags]",
Description: "A simple CLI for managing your tasks",
SubCommands: []*cli.Command{
{
Name: "list",
ShortHelp: "List all tasks",
Name: "list",
Description: "List all tasks",
Exec: func(ctx context.Context, s *cli.State) error {
// ...
return nil
Expand All @@ -96,7 +96,63 @@ example](examples/cmd/task/).
## Help

Help text is generated automatically and displayed when `--help` is passed. To customize it, set the
`UsageFunc` field on a command.
`Help` field on a command. It returns a string, so you can replace help entirely:

```go
Help: func(c *cli.Command) string {
return "Usage:\n greet <name>\n"
},
```

If you want to keep the default layout and add to it, use the optional `usage` package:

```go
Help: func(c *cli.Command) string {
doc := usage.New(c)
doc = append(doc, usage.Lines("Examples:", "greet margo"))
return doc.String()
},
```

That renders as:

```text
Usage:
greet

Examples:
greet margo
```

If you use `Parse` directly and need to handle help yourself, render help with the optional `usage`
package:

```go
if err := cli.Parse(root, args); err != nil {
if errors.Is(err, flag.ErrHelp) {
fmt.Fprintln(stdout, usage.Help(root))
return nil
}
return err
}
```

Inside `Exec`, `State` exposes the resolved command as `Cmd`, so usage errors can stay explicit:

```go
Exec: func(ctx context.Context, s *cli.State) error {
if len(s.Args) == 0 {
return cli.UsageErrorf("must supply a name")
}
fmt.Fprintf(s.Stdout, "hello, %s\n", s.Args[0])
return nil
},
```

`UsageErrorf` is opt-in: `Run` prints the resolved command's help to stderr before returning the
underlying error. Normal errors are returned unchanged.

For command-aware errors, use `s.Cmd.Path()` to get the resolved command path.

## Usage Syntax

Expand Down
75 changes: 36 additions & 39 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,54 @@ import (
"github.com/pressly/cli/pkg/suggest"
)

// ErrHelp is returned by [Parse] when the -help or -h flag is invoked. It is identical to
// [flag.ErrHelp] but re-exported here so callers using [Parse] and [Run] separately do not need to
// import the flag package solely for error checking.
// Command defines one command in a CLI.
//
// Note: [ParseAndRun] handles this automatically and never surfaces ErrHelp to the caller.
var ErrHelp = flag.ErrHelp

// Command represents a CLI command or subcommand within the application's command hierarchy.
// A command can be the root command passed to [ParseAndRun], or a subcommand listed in
// [Command.SubCommands]. Most programs define Name, optional help fields, optional flags, and Exec.
type Command struct {
// Name is always a single word representing the command's name. It is used to identify the
// command in the command hierarchy and in help text.
// Name is the single word users type to select the command.
Name string

// Usage provides the command's full usage pattern.
// Usage overrides the generated usage line.
//
// Example: "cli todo list [flags]"
Usage string

// ShortHelp is a brief description of the command's purpose. It is displayed in the help text
// when the command is shown.
ShortHelp string
// Description describes the command in help output and command lists.
Description string

// UsageFunc is an optional function that can be used to generate a custom usage string for the
// command. It receives the current command and should return a string with the full usage
// pattern.
UsageFunc func(*Command) string
// Help customizes the command's help text.
//
// Leave Help nil for the built-in help. Set it to replace help entirely. To keep the built-in
// layout and add sections, call usage.New from the usage package and return the document's
// String result.
Help func(*Command) string

// Flags holds the command-specific flag definitions. Each command maintains its own flag set
// for parsing arguments.
// Flags defines the command's flags using the standard library flag package.
Flags *flag.FlagSet
// FlagOptions is an optional list of flag options to extend the FlagSet with additional
// behavior. This is useful for tracking required flags, short aliases, and local flags.
FlagOptions []FlagOption

// SubCommands is a list of nested commands that exist under this command.
// FlagConfigs adds cli-specific behavior to flags already defined in Flags.
//
// Use it for required flags, short aliases, and flags that should not be inherited by
// subcommands.
FlagConfigs []FlagConfig

// SubCommands lists commands users can select after this command's name.
SubCommands []*Command

// Exec defines the command's execution logic. It receives the current application [State] and
// returns an error if execution fails. This function is called when [Run] is invoked on the
// command.
// Exec runs after parsing selects this command.
//
// Return [UsageErrorf] for invalid args or flag combinations so Run can print help. Return a
// normal error for operational failures.
Exec func(ctx context.Context, s *State) error

state *State
}

// Path returns the command chain from root to current command. It can only be called after the root
// command has been parsed and the command hierarchy has been established.
// Path returns the parsed command chain from root to this command.
//
// Call Path after [Parse] when command logic needs to inspect where the selected command sits in
// the command tree.
func (c *Command) Path() []*Command {
if c.state == nil {
return nil
Expand All @@ -71,26 +72,22 @@ func (c *Command) terminal() *Command {
return c.state.path[len(c.state.path)-1]
}

// FlagOption holds additional options for a flag, such as whether it is required or has a short
// alias.
type FlagOption struct {
// Name is the flag's name. Must match the flag name in the flag set.
// FlagConfig adds cli-specific behavior to a flag defined in a command's FlagSet.
type FlagConfig struct {
// Name is the flag's long name as registered in the command's FlagSet.
Name string

// Short is an optional single-character alias for the flag. When set, users can use either -v
// or -verbose (if Short is "v" and Name is "verbose"). Must be a single ASCII letter.
// Short lets users type a one-letter alias, such as -v for --verbose.
Short string

// Required indicates whether the flag is required.
// Required requires users to provide the flag explicitly.
Required bool

// Local indicates that the flag should not be inherited by child commands. When true, the flag
// is only available on the command that defines it.
// Local keeps the flag on this command instead of inheriting it into subcommands.
Local bool
}

// FlagsFunc is a helper function that creates a new [flag.FlagSet] and applies the given function
// to it. Intended for use in command definitions to simplify flag setup. Example usage:
// FlagsFunc creates a FlagSet inline for a command definition.
//
// cmd.Flags = cli.FlagsFunc(func(f *flag.FlagSet) {
// f.Bool("verbose", false, "enable verbose output")
Expand Down
6 changes: 3 additions & 3 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
// Quick example:
//
// root := &cli.Command{
// Name: "echo",
// Usage: "echo [flags] <text>...",
// ShortHelp: "prints the provided text",
// Name: "echo",
// Usage: "echo [flags] <text>...",
// Description: "prints the provided text",
// Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
// f.Bool("c", false, "capitalize the input")
// }),
Expand Down
24 changes: 24 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cli

import "fmt"

type usageError struct {
err error
}

// UsageErrorf returns an error for invalid command-line usage.
//
// Return UsageErrorf from Exec when the command was selected successfully but the remaining args or
// flag combination are invalid. [Run] prints command help to stderr, then returns the formatted
// error without the usage wrapper. Return a normal error when you do not want help printed.
func UsageErrorf(format string, args ...any) error {
return &usageError{err: fmt.Errorf(format, args...)}
}

func (e *usageError) Error() string {
return e.err.Error()
}

func (e *usageError) Unwrap() error {
return e.err
}
19 changes: 19 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cli

import (
"errors"
"testing"

"github.com/stretchr/testify/require"
)

func TestUsageError(t *testing.T) {
t.Parallel()

err := UsageErrorf("missing %s", "name")
require.EqualError(t, err, "missing name")

var usageErr *usageError
require.True(t, errors.As(err, &usageErr))
require.EqualError(t, errors.Unwrap(err), "missing name")
}
11 changes: 5 additions & 6 deletions examples/cmd/echo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"os"
Expand All @@ -13,19 +12,19 @@ import (

func main() {
root := &cli.Command{
Name: "echo",
Usage: "echo [flags] <text>...",
ShortHelp: "echo is a simple command that prints the provided text",
Name: "echo",
Usage: "echo [flags] <text>...",
Description: "echo is a simple command that prints the provided text",
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
// Add a flag to capitalize the input
f.Bool("c", false, "capitalize the input")
}),
FlagOptions: []cli.FlagOption{
FlagConfigs: []cli.FlagConfig{
{Name: "c", Required: true},
},
Exec: func(ctx context.Context, s *cli.State) error {
if len(s.Args) == 0 {
return errors.New("must provide text to echo, see --help")
return cli.UsageErrorf("must provide text to echo")
}
output := strings.Join(s.Args, " ")
// If -c flag is set, capitalize the output
Expand Down
Loading
Loading