Skip to content
Merged
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
15 changes: 7 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: stable
go-version: "1.27.x"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@9fae48acfc02a90574d7c304a1758ef9895495fa # v7.0.1

build:
name: Build and test
runs-on: ubuntu-latest
strategy:
matrix:
# go-version: ['oldstable', 'stable', '1.23.0-rc.2']
go-version: ["oldstable", "stable"]
go-version: ["1.27.x"]
env:
VERBOSE: 1

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: ${{ matrix.go-version }}
- name: Install tparse
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **BREAKING**: Replace the top-level `GetFlag` function with the generic `State.GetFlag` method
- **BREAKING**: Require Go 1.27 or higher

## [v0.7.0] - 2026-05-14

### Added
Expand Down
196 changes: 18 additions & 178 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
[![CI](https://github.com/pressly/cli/actions/workflows/ci.yaml/badge.svg)](https://github.com/pressly/cli/actions/workflows/ci.yaml)
[![Docs](https://img.shields.io/badge/docs-pressly.github.io%2Fcli-blue)](https://pressly.github.io/cli)

An intentionally minimal Go package for building CLI applications. Extends the standard library's
`flag` package to support [flags
anywhere](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/) in command arguments,
adds nested subcommands, and gets out of the way.
An intentionally minimal Go package for building CLI applications. It extends the standard library's
`flag` package with nested subcommands and [flags
anywhere](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/), then gets out of the
way.

Docs: <https://pressly.github.io/cli>

Expand All @@ -19,24 +19,21 @@ Docs: <https://pressly.github.io/cli>
go get github.com/pressly/cli@latest
```

Requires Go 1.21 or higher.
Requires Go 1.27 or higher.

## Quick Start

```go
root := &cli.Command{
Name: "echo",
Usage: "echo [flags] <text>...",
Summary: "Print text",
Name: "echo",
Usage: "echo [flags] <text>...",
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
f.Bool("capitalize", false, "capitalize the input")
}),
FlagConfigs: []cli.FlagConfig{
{Name: "capitalize", Short: "c"},
},
Exec: func(ctx context.Context, s *cli.State) error {
text := strings.Join(s.Args, " ")
if cli.GetFlag[bool](s, "capitalize") {
// GetFlag uses generic methods, available in Go 1.27.
if s.GetFlag[bool]("capitalize") {
text = strings.ToUpper(text)
}
fmt.Fprintln(s.Stdout, text)
Expand All @@ -49,187 +46,30 @@ if err := cli.ParseAndRun(ctx, root, os.Args[1:], nil); err != nil {
}
```

`ParseAndRun` parses the command hierarchy, handles `--help` automatically, and executes the
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.
`ParseAndRun` parses the command hierarchy, handles `--help`, and runs the selected command.

The command above gets usable help without any extra setup:
The command above gets usable help without extra setup:

```text
Print text

Usage:
echo [flags] <text>...

Flags:
-c, --capitalize capitalize the input
```

## Flags

`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")
}),
FlagConfigs: []cli.FlagConfig{
{Name: "verbose", Short: "v"},
{Name: "output", Short: "o", Required: true},
},
```

Short aliases register `-v` as an alias for `--verbose`, `-o` as an alias for `--output`, and so on.
Both forms are shown in help output automatically.

Access flags inside `Exec` with the type-safe `GetFlag` function:

```go
verbose := cli.GetFlag[bool](s, "verbose")
output := cli.GetFlag[string](s, "output")
--capitalize capitalize the input
```

Child commands automatically inherit flags from parent commands, so a `--verbose` flag on the root
is accessible from any subcommand via `GetFlag`.

## Subcommands

Commands can have nested subcommands, each with their own flags and `Exec` function:

```go
root := &cli.Command{
Name: "todo",
Usage: "todo <command> [flags]",
Summary: "Manage tasks",
Description: "todo manages tasks stored in a local file.",
SubCommands: []*cli.Command{
{
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
},
},
},
}
```

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

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

When a command only groups subcommands, leave `Exec` unset. Selecting it without a child command
returns a usage error and shows that command's help.

`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. 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 Strings

Set `Command.Usage` when the default usage line is too broad. A common convention is to write
required values as `<name>`, optional values as `[name]`, and repeated values with `...`:

```go
Usage: "echo [flags] <text>..."
```

## Status

This project is in active development and undergoing changes as the API gets refined. Please open an
issue if you encounter any problems or have suggestions for improvement.
For subcommands, inherited, local, and required flags, custom help, usage errors, and subpackages,
see the [documentation](https://pressly.github.io/cli). More complete programs live in
[examples](examples/).

## Acknowledgements

There are many great CLI libraries out there, but I always felt [they were too heavy for my
needs](https://mfridman.com/blog/2021/a-simpler-building-block-for-go-clis/).

Inspired by Peter Bourgon's [ff](https://github.com/peterbourgon/ff) library, specifically the `v3`
branch, which was so close to what I wanted. The `v4` branch took a different direction, and I
wanted to keep the simplicity of `v3`. This library carries that idea forward.
Inspired by Peter Bourgon's [ff](https://github.com/peterbourgon/ff) library, especially its `v3`
branch, which was close to what I wanted. `v4` took a different direction, but I wanted to keep the
simplicity of `v3`. This library carries that idea forward.

## License

Expand Down
14 changes: 7 additions & 7 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// - Nested subcommands via [Command.SubCommands]
// - Flags placed anywhere on the command line
// - Parent flags inherited by child commands
// - Type-safe flag access via [GetFlag]
// - Type-safe flag access via [State.GetFlag]
// - Generated help, replaceable per command via [Command.Help]
// - "Did you mean" suggestions for misspelled subcommands
//
Expand All @@ -21,7 +21,7 @@
// }),
// Exec: func(ctx context.Context, s *cli.State) error {
// output := strings.Join(s.Args, " ")
// if cli.GetFlag[bool](s, "c") {
// if s.GetFlag[bool]("c") {
// output = strings.ToUpper(output)
// }
// fmt.Fprintln(s.Stdout, output)
Expand Down Expand Up @@ -103,7 +103,7 @@ type Command struct {
// [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].
// [Command.FlagConfigs]. Read flag values inside [Command.Exec] with [State.GetFlag].
Flags *flag.FlagSet

// FlagConfigs adds extra behavior to flags already defined in [Command.Flags]. See [FlagConfig]
Expand Down Expand Up @@ -221,10 +221,10 @@ func FlagsFunc(fn func(f *flag.FlagSet)) (fset *flag.FlagSet) {
// defined on the root command can be read from any subcommand. An unknown flag name or a wrong type
// is a programming error: GetFlag panics, and [Run] catches the panic and returns the error.
//
// verbose := cli.GetFlag[bool](s, "verbose")
// count := cli.GetFlag[int](s, "count")
// path := cli.GetFlag[string](s, "path")
func GetFlag[T any](s *State, name string) T {
// verbose := s.GetFlag[bool]("verbose")
// count := s.GetFlag[int]("count")
// path := s.GetFlag[string]("path")
func (s *State) GetFlag[T any](name string) T {
if s == nil {
panic(&internalError{err: errors.New("state is nil")})
}
Expand Down
Loading
Loading