Skip to content

Commit fc8c5dd

Browse files
committed
docs: simplify README and extract usage syntax conventions
1 parent 7344d9c commit fc8c5dd

File tree

2 files changed

+84
-185
lines changed

2 files changed

+84
-185
lines changed

README.md

Lines changed: 53 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -3,230 +3,98 @@
33
[![GoDoc](https://godoc.org/github.com/pressly/cli?status.svg)](https://pkg.go.dev/github.com/pressly/cli#pkg-index)
44
[![CI](https://github.com/pressly/cli/actions/workflows/ci.yaml/badge.svg)](https://github.com/pressly/cli/actions/workflows/ci.yaml)
55

6-
A Go package for building CLI applications. Extends the standard library's `flag` package to support
7-
[flags anywhere](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/) in command
8-
arguments.
9-
10-
## Features
11-
12-
The **bare minimum** to build a CLI application while leveraging the standard library's `flag`
13-
package.
14-
15-
- Nested subcommands for organizing complex CLIs
16-
- Flexible flag parsing, allowing flags anywhere
17-
- Subcommands inherit flags from parent commands
18-
- Type-safe flag access
19-
- Automatic generation of help text and usage information
20-
- Suggestions for misspelled or incomplete commands
21-
22-
### But why?
23-
24-
This package is intentionally minimal. It aims to be a building block for CLI applications that want
25-
to leverage the standard library's `flag` package while providing a bit more structure and
26-
flexibility.
27-
28-
- Build maintainable command-line tools quickly
29-
- Focus on application logic rather than framework complexity
30-
- Extend functionality **only when needed**
31-
32-
Sometimes less is more. While other frameworks offer extensive features, this package focuses on
33-
core functionality.
6+
An intentionally minimal Go package for building CLI applications. Extends the standard library's
7+
`flag` package to support [flags
8+
anywhere](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/) in command arguments,
9+
adds nested subcommands, and gets out of the way.
3410

3511
## Installation
3612

3713
```bash
3814
go get github.com/pressly/cli@latest
3915
```
4016

41-
Required go version: 1.21 or higher
17+
Requires Go 1.21 or higher.
4218

4319
## Quick Start
4420

45-
Here's a simple example of a CLI application that echoes back the input with a required `-c` flag to
46-
capitalize the output:
47-
4821
```go
4922
root := &cli.Command{
50-
Name: "echo",
51-
Usage: "echo [flags] <text>...",
52-
ShortHelp: "echo is a simple command that prints the provided text",
53-
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
54-
// Add a flag to capitalize the input
55-
f.Bool("c", false, "capitalize the input")
56-
}),
57-
FlagsMetadata: []cli.FlagMetadata{
58-
{Name: "c", Required: true},
59-
},
23+
Name: "greet",
24+
ShortHelp: "Print a greeting",
6025
Exec: func(ctx context.Context, s *cli.State) error {
61-
if len(s.Args) == 0 {
62-
return errors.New("must provide text to echo, see --help")
63-
}
64-
output := strings.Join(s.Args, " ")
65-
// If -c flag is set, capitalize the output
66-
if cli.GetFlag[bool](s, "c") {
67-
output = strings.ToUpper(output)
68-
}
69-
fmt.Fprintln(s.Stdout, output)
26+
fmt.Fprintln(s.Stdout, "hello, world!")
7027
return nil
7128
},
7229
}
73-
if err := cli.Parse(root, os.Args[1:]); err != nil {
74-
if errors.Is(err, flag.ErrHelp) {
75-
fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root))
76-
return
77-
}
78-
fmt.Fprintf(os.Stderr, "error: %v\n", err)
79-
os.Exit(1)
80-
}
81-
if err := cli.Run(context.Background(), root, nil); err != nil {
30+
if err := cli.ParseAndRun(ctx, root, os.Args[1:], nil); err != nil {
8231
fmt.Fprintf(os.Stderr, "error: %v\n", err)
8332
os.Exit(1)
8433
}
8534
```
8635

87-
## Command Structure
88-
89-
Each command is represented by a `Command` struct:
90-
91-
```go
92-
type Command struct {
93-
Name string // Required
94-
Usage string
95-
ShortHelp string
96-
UsageFunc func(*Command) string
97-
Flags *flag.FlagSet
98-
FlagsMetadata []FlagMetadata
99-
SubCommands []*Command
100-
Exec func(ctx context.Context, s *State) error
101-
}
102-
```
103-
104-
The `Name` field is the command's name and is **required**.
105-
106-
The `Usage` and `ShortHelp` fields are used to generate help text. Nice-to-have but not required.
107-
108-
The `Flags` field is a `*flag.FlagSet` that defines the command's flags.
109-
110-
> [!TIP]
111-
>
112-
> There's a convenience function `FlagsFunc` that allows you to define flags inline:
113-
114-
```go
115-
root := &cli.Command{
116-
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
117-
fs.Bool("verbose", false, "enable verbose output")
118-
fs.String("output", "", "output file")
119-
fs.Int("count", 0, "number of items")
120-
}),
121-
FlagsMetadata: []cli.FlagMetadata{
122-
{Name: "c", Required: true},
123-
},
124-
}
125-
```
126-
127-
The optional `FlagsMetadata` field is a way to extend defined flags. The `flag` package alone is a
128-
bit limiting, so we add this to provide the most common features, such as handling of required
129-
flags.
130-
131-
The `SubCommands` field is a list of `*Command` structs that represent subcommands. This allows you
132-
to organize CLI applications into a hierarchy of commands. Each subcommand can have its own flags
133-
and business logic.
134-
135-
The `Exec` field is a function that is called when the command is executed. This is where you put
136-
business logic.
36+
`ParseAndRun` parses the command hierarchy, handles `--help` automatically, and executes the
37+
resolved command. For applications that need work between parsing and execution, use `Parse` and
38+
`Run` separately. See the [examples](examples/) directory for more complete applications.
13739

138-
## Flag Access
40+
## Flags
13941

140-
Flags can be accessed using the type-safe `GetFlag` function, called inside the `Exec` function:
42+
`FlagsFunc` is a convenience for defining flags inline. Use `FlagsMetadata` to extend the standard
43+
`flag` package with features like required flag enforcement:
14144

14245
```go
143-
// Access boolean flag
144-
verbose := cli.GetFlag[bool](state, "verbose")
145-
// Access string flag
146-
output := cli.GetFlag[string](state, "output")
147-
// Access integer flag
148-
count := cli.GetFlag[int](state, "count")
46+
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
47+
f.Bool("verbose", false, "enable verbose output")
48+
f.String("output", "", "output file")
49+
}),
50+
FlagsMetadata: []cli.FlagMetadata{
51+
{Name: "output", Required: true},
52+
},
14953
```
15054

151-
### State Inheritance
152-
153-
Child commands automatically inherit their parent command's flags:
55+
Access flags inside `Exec` with the type-safe `GetFlag` function:
15456

15557
```go
156-
// Parent command with a verbose flag
157-
root := cli.Command{
158-
Name: "root",
159-
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
160-
f.Bool("verbose", false, "enable verbose mode")
161-
}),
162-
}
163-
164-
// Child command that can access parent's verbose flag
165-
sub := cli.Command{
166-
Name: "sub",
167-
Exec: func(ctx context.Context, s *cli.State) error {
168-
verbose := cli.GetFlag[bool](s, "verbose")
169-
if verbose {
170-
fmt.Println("Verbose mode enabled")
171-
}
172-
return nil
173-
},
174-
}
58+
verbose := cli.GetFlag[bool](s, "verbose")
59+
output := cli.GetFlag[string](s, "output")
17560
```
17661

177-
## Help System
62+
Child commands automatically inherit flags from parent commands, so a `--verbose` flag on the root
63+
is accessible from any subcommand via `GetFlag`.
17864

179-
Help text is automatically generated, but you can customize it by setting the `UsageFunc` field.
65+
## Subcommands
18066

181-
There is a `DefaultUsage` function that generates a default help text for a command, which is useful
182-
to display when `flag.ErrHelp` is returned from `Parse`:
67+
Commands can have nested subcommands, each with their own flags and `Exec` function:
18368

18469
```go
185-
if err := cli.Parse(root, os.Args[1:]); err != nil {
186-
if errors.Is(err, flag.ErrHelp) {
187-
fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root)) // Display help text and exit
188-
return
189-
}
190-
fmt.Fprintf(os.Stderr, "error: %v\n", err)
191-
os.Exit(1)
70+
root := &cli.Command{
71+
Name: "todo",
72+
Usage: "todo <command> [flags]",
73+
ShortHelp: "A simple CLI for managing your tasks",
74+
SubCommands: []*cli.Command{
75+
{
76+
Name: "list",
77+
ShortHelp: "List all tasks",
78+
Exec: func(ctx context.Context, s *cli.State) error {
79+
// ...
80+
return nil
81+
},
82+
},
83+
},
19284
}
19385
```
19486

195-
## Usage Syntax Conventions
196-
197-
When reading command usage strings, the following syntax is used:
87+
For a more complete example with deeply nested subcommands, see the [todo
88+
example](examples/cmd/task/).
19889

199-
| Syntax | Description |
200-
| ------------- | -------------------------- |
201-
| `<required>` | Required argument |
202-
| `[optional]` | Optional argument |
203-
| `<arg>...` | One or more arguments |
204-
| `[arg]...` | Zero or more arguments |
205-
| `(a\|b)` | Must choose one of a or b |
206-
| `[-f <file>]` | Flag with value (optional) |
207-
| `-f <file>` | Flag with value (required) |
90+
## Help
20891

209-
Examples:
92+
Help text is generated automatically and displayed when `--help` is passed. To customize it, set the
93+
`UsageFunc` field on a command.
21094

211-
```bash
212-
# Multiple source files, one destination
213-
mv <source>... <dest>
214-
215-
# Required flag with value, optional config
216-
build -t <tag> [config]...
217-
218-
# Subcommands with own flags
219-
docker (run|build) [--file <dockerfile>] <image>
95+
## Usage Syntax
22096

221-
# Multiple flag values
222-
find [--exclude <pattern>]... <path>
223-
224-
# Choice between options, required path
225-
chmod (u+x|a+r) <file>...
226-
227-
# Flag groups with value
228-
kubectl [-n <namespace>] (get|delete) (pod|service) <name>
229-
```
97+
See [docs/usage-syntax.md](docs/usage-syntax.md) for conventions used in usage strings.
23098

23199
## Status
232100

@@ -238,9 +106,9 @@ issue if you encounter any problems or have suggestions for improvement.
238106
There are many great CLI libraries out there, but I always felt [they were too heavy for my
239107
needs](https://mfridman.com/blog/2021/a-simpler-building-block-for-go-clis/).
240108

241-
I was inspired by Peter Bourgon's [ff](https://github.com/peterbourgon/ff) library, specifically the
242-
`v3` branch, which was soooo close to what I wanted. But the `v4` branch took a different direction
243-
and I wanted to keep the simplicity of `v3`. This library aims to pick up where `v3` left off.
109+
Inspired by Peter Bourgon's [ff](https://github.com/peterbourgon/ff) library, specifically the `v3`
110+
branch, which was so close to what I wanted. The `v4` branch took a different direction, and I wanted
111+
to keep the simplicity of `v3`. This library carries that idea forward.
244112

245113
## License
246114

docs/usage-syntax.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Usage Syntax Conventions
2+
3+
These conventions follow the
4+
[POSIX utility argument syntax](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html)
5+
and are widely used by tools like `docker`, `kubectl`, and `git`.
6+
7+
| Syntax | Description |
8+
| ------------- | ------------------------- |
9+
| `<required>` | Required argument |
10+
| `[optional]` | Optional argument |
11+
| `<arg>...` | One or more arguments |
12+
| `[arg]...` | Zero or more arguments |
13+
| `(a\|b)` | Must choose one of a or b |
14+
| `[-f <value>]`| Optional flag with value |
15+
| `-f <value>` | Required flag with value |
16+
17+
## Examples
18+
19+
```
20+
# Positional arguments
21+
cp <source>... <dest>
22+
23+
# Mix of required flag and optional positional args
24+
build -t <tag> [config]...
25+
26+
# Subcommand with optional flag
27+
app (start|stop) [-n <name>]
28+
29+
# Repeatable optional flag
30+
search [--exclude <pattern>]... <path>
31+
```

0 commit comments

Comments
 (0)