Skip to content

Commit c8e5022

Browse files
committed
Improve bool handling; update README.md
1 parent 093890f commit c8e5022

File tree

3 files changed

+59
-40
lines changed

3 files changed

+59
-40
lines changed

README.md

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,27 @@
33
[![GoDoc](https://godoc.org/github.com/mfridman/cli?status.svg)](https://pkg.go.dev/github.com/mfridman/cli#section-documentation)
44
[![CI](https://github.com/mfridman/cli/actions/workflows/ci.yaml/badge.svg)](https://github.com/mfridman/cli/actions/workflows/ci.yaml)
55

6-
A Go framework for building CLI applications with flexible flag placement. Extends the standard
7-
library's `flag` package to support [flags
8-
anywhere](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/) in command arguments.
6+
A Go framework for building CLI applications. Extends the standard library's `flag` package to
7+
support [flags anywhere](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/) in
8+
command arguments.
99

1010
## Features
1111

12+
The **bare minimum** to build a CLI application while leveraging the standard library's `flag`
13+
package.
14+
1215
- Nested subcommands for organizing complex CLIs
1316
- Flexible flag parsing, allowing flags anywhere
1417
- Subcommands inherit flags from parent commands
1518
- Type-safe flag access
1619
- Automatic generation of help text and usage information
1720
- Suggestions for misspelled or incomplete commands
1821

19-
And that's it! It's the **bare minimum to build a CLI application** while leveraging the standard
20-
library's `flag` package.
21-
2222
### But why?
2323

24-
This framework embraces minimalism while maintaining functionality. It provides essential building
25-
blocks for CLI applications without the bloat, allowing you to:
24+
This framework is intentionally minimal. It aims to be a building block for CLI applications that
25+
want to leverage the standard library's `flag` package while providing a bit more structure and
26+
flexibility.
2627

2728
- Build maintainable command-line tools quickly
2829
- Focus on application logic rather than framework complexity
@@ -46,7 +47,7 @@ Here's a simple example of a CLI application that echoes back the input:
4647
```go
4748
root := &cli.Command{
4849
Name: "echo",
49-
Usage: "echo <text...> [flags]",
50+
Usage: "echo [flags] <text>...",
5051
ShortHelp: "echo is a simple command that prints the provided text",
5152
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
5253
// Add a flag to capitalize the input
@@ -57,8 +58,7 @@ root := &cli.Command{
5758
},
5859
Exec: func(ctx context.Context, s *cli.State) error {
5960
if len(s.Args) == 0 {
60-
// Return a new error with the error code ErrShowHelp
61-
return fmt.Errorf("no text provided")
61+
return errors.New("must provide text to echo, see --help")
6262
}
6363
output := strings.Join(s.Args, " ")
6464
// If -c flag is set, capitalize the output
@@ -69,8 +69,7 @@ root := &cli.Command{
6969
return nil
7070
},
7171
}
72-
err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil)
73-
if err != nil {
72+
if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil {
7473
if errors.Is(err, flag.ErrHelp) {
7574
return
7675
}
@@ -88,7 +87,7 @@ Each command in your CLI application is represented by a `Command` struct:
8887

8988
```go
9089
type Command struct {
91-
Name string
90+
Name string // Required
9291
Usage string
9392
ShortHelp string
9493
UsageFunc func(*Command) string
@@ -103,8 +102,7 @@ The `Name` field is the command's name and is **required**.
103102

104103
The `Usage` and `ShortHelp` fields are used to generate help text. Nice-to-have but not required.
105104

106-
The `Flags` field is a `*flag.FlagSet` that defines the command's flags. The `SubCommands` field is
107-
a slice of child commands.
105+
The `Flags` field is a `*flag.FlagSet` that defines the command's flags.
108106

109107
> [!TIP]
110108
>
@@ -124,8 +122,12 @@ root := &cli.Command{
124122
```
125123

126124
The `FlagsMetadata` field is a slice of `FlagMetadata` structs that define metadata for each flag.
127-
Unfortunatly, the `flag.FlagSet` package alone is a bit limiting, so this package adds a layer on
128-
top to provide the most common features.
125+
Unfortunatly, the `flag` package alone is a bit limiting, so this package adds a layer on top to
126+
provide the most common features, such as automatic handling of required flags.
127+
128+
The `SubCommands` field is a slice of `*Command` structs that represent subcommands. This allows you
129+
to organize your CLI application into a hierarchy of commands. Each subcommand can have its own
130+
flags and business logic.
129131

130132
The `Exec` field is a function that is called when the command is executed. This is where you put
131133
your business logic.
@@ -137,10 +139,8 @@ Flags can be accessed using the type-safe `GetFlag` function, called inside your
137139
```go
138140
// Access boolean flag
139141
verbose := cli.GetFlag[bool](state, "verbose")
140-
141142
// Access string flag
142143
output := cli.GetFlag[string](state, "output")
143-
144144
// Access integer flag
145145
count := cli.GetFlag[int](state, "count")
146146
```
@@ -183,36 +183,45 @@ When reading command usage strings, the following syntax is used:
183183
| ------------- | -------------------------- |
184184
| `<required>` | Required argument |
185185
| `[optional]` | Optional argument |
186-
| `<arg...>` | One or more arguments |
187-
| `[arg...]` | Zero or more arguments |
186+
| `<arg>...` | One or more arguments |
187+
| `[arg]...` | Zero or more arguments |
188188
| `(a\|b)` | Must choose one of a or b |
189189
| `[-f <file>]` | Flag with value (optional) |
190190
| `-f <file>` | Flag with value (required) |
191191

192192
Examples:
193193

194194
```bash
195-
# Two required arguments
196-
copy <source> <dest>
197-
# Zero or more paths
198-
ls [path...]
199-
# Optional flag with value, required host
200-
ssh [-p <port>] <user@host>
201-
# Required subcommand, optional remote
202-
git (pull|push) [remote]
195+
# Multiple source files, one destination
196+
mv <source>... <dest>
197+
198+
# Required flag with value, optional config
199+
build -t <tag> [config]...
200+
201+
# Subcommands with own flags
202+
docker (run|build) [--file <dockerfile>] <image>
203+
204+
# Multiple flag values
205+
find [--exclude <pattern>]... <path>
206+
207+
# Choice between options, required path
208+
chmod (u+x|a+r) <file>...
209+
210+
# Flag groups with value
211+
kubectl [-n <namespace>] (get|delete) (pod|service) <name>
203212
```
204213

205214
## Status
206215

207-
This project is in active development and undergoing changes as the API is refined. Please open an
216+
This project is in active development and undergoing changes as the API gets refined. Please open an
208217
issue if you encounter any problems or have suggestions for improvement.
209218

210219
- [x] Nail down required flags implementation
211-
- [ ] Add tests for typos and command suggestions, crude levenstein distance for now
212-
- [ ] Internal implementation (not user-facing), track selected `*Command` in `*State` and remove
220+
- [x] Add tests for typos and command suggestions, crude levenstein distance for now
221+
- [x] Internal implementation (not user-facing), track selected `*Command` in `*State` and remove
213222
`flags *flag.FlagSet` from `*State`
214-
- [ ] Figure out whether to keep `*Error` and whether to catch `ErrShowHelp` in `ParseAndRun`
215-
- [ ] Should `Parse`, `Run` and `ParseAndRun` be methods on `*Command`?
223+
- [x] Figure out whether to keep `*Error` and whether to catch `ErrShowHelp` in `ParseAndRun`
224+
- [x] Should `Parse`, `Run` and `ParseAndRun` be methods on `*Command`? No.
216225
- [ ] What to do with `showHelp()`, should it be a standalone function or an exported method on
217226
`*Command`?
218227
- [ ] Is there room for `clihelp` package for standalone use?
@@ -224,7 +233,7 @@ needs](https://mfridman.com/blog/2021/a-simpler-building-block-for-go-clis/).
224233

225234
I was inspired by Peter Bourgon's [ff](https://github.com/peterbourgon/ff) library, specifically the
226235
`v3` branch, which was soooo close to what I wanted. But the `v4` branch took a different direction
227-
and I wanted to keep the simplicity of the `v3` branch. This library aims to pick up where `ff/v3`
236+
and I wanted to keep the simplicity of the `v3` branch. This library aims to pick up where the `v3`
228237
left off.
229238

230239
## License

examples/cmd/echo/main.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func main() {
1515
root := &cli.Command{
1616
Name: "echo",
17-
Usage: "echo <text...> [flags]",
17+
Usage: "echo [flags] <text>...",
1818
ShortHelp: "echo is a simple command that prints the provided text",
1919
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
2020
// Add a flag to capitalize the input
@@ -36,8 +36,7 @@ func main() {
3636
return nil
3737
},
3838
}
39-
err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil)
40-
if err != nil {
39+
if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil {
4140
if errors.Is(err, flag.ErrHelp) {
4241
return
4342
}

parse.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,18 @@ func Parse(root *Command, args []string) error {
118118
if flag == nil {
119119
return fmt.Errorf("command %q: internal error: required flag %s not found in flag set", getCommandPath(root.state.commandPath), formatFlagName(flagMetadata.Name))
120120
}
121-
if flag.Value.String() == flag.DefValue {
121+
if _, isBool := flag.Value.(interface{ IsBoolFlag() bool }); isBool {
122+
isSet := false
123+
for _, arg := range argsToParse {
124+
if strings.HasPrefix(arg, "-"+flagMetadata.Name) || strings.HasPrefix(arg, "--"+flagMetadata.Name) {
125+
isSet = true
126+
break
127+
}
128+
}
129+
if !isSet {
130+
missingFlags = append(missingFlags, formatFlagName(flagMetadata.Name))
131+
}
132+
} else if flag.Value.String() == flag.DefValue {
122133
missingFlags = append(missingFlags, formatFlagName(flagMetadata.Name))
123134
}
124135
}

0 commit comments

Comments
 (0)