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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
internal/
tmp/
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- `flagtype.EnumDefault` constructor for enums with an initial default value
- `Cmd` field on `State` for accessing the terminal command selected by parsing
- `Summary` field on `Command` for the short text shown in command lists
- `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**: Replace `Command.ShortHelp` with `Command.Summary` for command lists and
`Command.Description` for longer command help text
- **BREAKING**: Rename `FlagOption` to `FlagConfig` and `Command.FlagOptions` to
`Command.FlagConfigs`
- Help output keeps the default automatic `--help` behavior through `ParseAndRun`; `Command.Help`
replaces the generated help string when a command needs full control

### Removed

- **BREAKING**: Remove `ErrHelp`; check `errors.Is(err, flag.ErrHelp)` when handling `Parse`
directly
- **BREAKING**: Remove `DefaultUsage` and the top-level `Usage` function from the public API

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

Expand Down
112 changes: 101 additions & 11 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",
Summary: "Print a greeting",
Exec: func(ctx context.Context, s *cli.State) error {
fmt.Fprintln(s.Stdout, "hello, world!")
return nil
Expand All @@ -39,17 +39,26 @@ if err := cli.ParseAndRun(ctx, root, os.Args[1:], nil); err != nil {
resolved command. For applications that need work between parsing and execution, use `Parse` and
`Run` separately. See the [examples](examples/) directory for more complete applications.

The command above gets usable help without any extra setup:

```text
Print a greeting

Usage:
greet
```

## 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 +83,17 @@ 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]",
Summary: "Manage tasks",
Description: "todo manages tasks stored in a local file.",
SubCommands: []*cli.Command{
{
Name: "list",
ShortHelp: "List all tasks",
Name: "list",
Summary: "List tasks",
Description: `List tasks in the current workspace.

By default, completed tasks are hidden.`,
Exec: func(ctx context.Context, s *cli.State) error {
// ...
return nil
Expand All @@ -90,13 +103,90 @@ root := &cli.Command{
}
```

`Summary` is the short sentence shown when a command appears in another command's help:

```text
Available Commands:
list List tasks
```

`Description` is the longer text shown at the top of that command's own help:

```text
List tasks in the current workspace.

By default, completed tasks are hidden.

Usage:
todo list
```

If a command only needs one sentence, set `Summary` and leave `Description` empty. If `Description`
is set and `Summary` is empty, command lists use the first line of `Description`.

For a more complete example with deeply nested subcommands, see the [todo
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 text is generated automatically and displayed when `--help` is passed. Most commands only need
`Name`, `Summary`, flags, subcommands, and `Exec`.

Set the `Help` field only when a command needs to replace the generated help entirely:

```go
Help: func(c *cli.Command) string {
return `Print a greeting.

Usage:
greet <name>

Examples:
greet margo`
},
```

That replaces the built-in help with:

```text
Print a greeting.

Usage:
greet <name>

Examples:
greet margo
```

If you use `Parse` directly, handle `flag.ErrHelp` yourself. Most applications should use
`ParseAndRun` when they want cli to print help automatically.

```go
if err := cli.Parse(root, args); err != nil {
if errors.Is(err, flag.ErrHelp) {
// Print custom help here, or use ParseAndRun for built-in help.
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
112 changes: 70 additions & 42 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,79 @@ 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 describes a single command in the 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.
// Pass a Command to [ParseAndRun] (or [Parse] and [Run]) to run a program. To add a subcommand,
// list it in another command's [Command.SubCommands].
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 word users type to pick this command. It must start with a letter and can contain
// letters, digits, dashes, or underscores. For the root command it is also the program name
// shown in help.
Name string

// Usage provides the command's full usage pattern.
// Usage replaces the usage line shown at the top of help. Set it to show the expected
// arguments. The default usage line shows only the command path, plus "[flags]" when the
// command has flags.
//
// Example: "cli todo list [flags]"
// Example: "todo list <view> [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
// Summary is the one-line description shown next to this command in its parent's command list.
// It is also shown at the top of this command's own help when [Command.Description] is empty.
//
// Most commands only need Summary. Use [Command.Description] when one line is not enough.
Summary string

// Description is the longer help text shown at the top of this command's own help. Use it to
// explain behavior, defaults, or anything else worth knowing.
//
// When [Command.Summary] is empty, the first line of Description is used in command lists
// instead.
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 replaces the built-in help text for this command. Leave it nil to use the default help.
//
// The function is given the command and returns the full help string. Help is used for --help
// and for [UsageErrorf] errors. Each command can set its own Help, and only the selected
// command's Help is called.
Help func(*Command) string

// Flags holds the command-specific flag definitions. Each command maintains its own flag set
// for parsing arguments.
// Flags holds this command's flags as a standard library [flag.FlagSet]. Build it with
// [flag.NewFlagSet], or use [FlagsFunc] to define flags inline.
//
// Subcommands inherit these flags unless they are marked [FlagConfig.Local] in
// [Command.FlagConfigs]. Read flag values inside [Command.Exec] with [GetFlag].
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 extra behavior to flags already defined in [Command.Flags]. See [FlagConfig]
// for the available options.
//
// Each entry must point to a flag defined in [Command.Flags]. Otherwise [Parse] returns an
// error.
FlagConfigs []FlagConfig

// SubCommands are the commands users can pick after this command's name.
//
// When a command has SubCommands, the first non-flag argument must match one of them. An
// unknown name returns an "unknown command" error with suggestions. Commands without
// SubCommands pass any non-flag arguments through to [State.Args].
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 is the function that runs when this command is picked. It is given a [State] holding the
// parsed inputs the command needs.
//
// Return [UsageErrorf] for bad arguments or flag combinations so [Run] prints the command's
// help to stderr. Return a normal error for everything else; [Run] returns it without printing
// help.
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 list of commands from the root down to this command. It is usually called inside
// [Command.Exec] as s.Cmd.Path() to build error messages that include the full command path.
//
// Path returns nil if called before [Parse].
func (c *Command) Path() []*Command {
if c.state == nil {
return nil
Expand All @@ -71,32 +97,34 @@ 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 extra behavior to a single flag already defined in [Command.Flags]. It is used as
// an entry in [Command.FlagConfigs].
type FlagConfig struct {
// Name is the long flag name as registered in the command's [flag.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 is a one-letter alias for the flag, such as "v" so users can type -v instead of
// --verbose. Both forms are shown in help.
Short string

// Required indicates whether the flag is required.
// Required, when true, makes [Parse] fail unless the user sets the flag. The default value is
// not enough; the user must pass it.
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, when true, keeps the flag on this command only and stops it from being inherited by
// subcommands. Parent flags are inherited by default.
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 [flag.FlagSet] inline so you don't have to make one and assign it separately.
// The returned FlagSet uses [flag.ContinueOnError], so parsing errors are returned instead of being
// fatal.
//
// cmd.Flags = cli.FlagsFunc(func(f *flag.FlagSet) {
// Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
// f.Bool("verbose", false, "enable verbose output")
// f.String("output", "", "output file")
// f.Int("count", 0, "number of items")
// })
// }),
func FlagsFunc(fn func(f *flag.FlagSet)) *flag.FlagSet {
fset := flag.NewFlagSet("", flag.ContinueOnError)
fn(fset)
Expand Down
36 changes: 19 additions & 17 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// Package cli provides a lightweight library for building command-line applications using Go's
// standard library flag package. It extends flag functionality to support flags anywhere in command
// arguments.
// Package cli builds command-line programs on top of the standard library [flag] package. It adds
// nested subcommands and lets users place flags anywhere in command arguments.
//
// Key features:
// - Nested subcommands for organizing complex CLIs
// - Flexible flag parsing, allowing flags anywhere in arguments
// - Parent-to-child flag inheritance
// - Type-safe flag access
// - Automatic help text generation
// - Command suggestions for misspelled inputs
// Features:
// - Nested subcommands via [Command.SubCommands]
// - Flags placed anywhere on the command line
// - Parent flags inherited by child commands
// - Type-safe flag access via [GetFlag]
// - Generated help, replaceable per command via [Command.Help]
// - "Did you mean" suggestions for misspelled subcommands
//
// Quick example:
//
// root := &cli.Command{
// Name: "echo",
// Usage: "echo [flags] <text>...",
// ShortHelp: "prints the provided text",
// Name: "echo",
// Usage: "echo [flags] <text>...",
// Summary: "Print text",
// Description: "echo prints the provided text.",
// Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
// f.Bool("c", false, "capitalize the input")
// }),
Expand All @@ -28,9 +28,11 @@
// return nil
// },
// }
// if err := cli.ParseAndRun(ctx, root, os.Args[1:], nil); err != nil {
// fmt.Fprintf(os.Stderr, "error: %v\n", err)
// os.Exit(1)
// }
//
// The package intentionally maintains a minimal API surface to serve as a building block for CLI
// applications while leveraging the standard library's flag package. This approach enables
// developers to build maintainable command-line tools quickly while focusing on application logic
// rather than framework complexity.
// The API is small on purpose. cli uses the standard library flag package instead of replacing it,
// so most of what you write is your program.
package cli
Loading
Loading