Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some docs and README improvements #80

Merged
merged 5 commits into from
Aug 11, 2024
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
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Tiny, simple, but powerful CLI framework for modern Go 🚀

<p align="center">
<img src="./docs/img/demo.png" alt="demo">
<img src="https://github.com/FollowTheProcess/cli/raw/main/docs/img/demo.png" alt="demo">
</p>

> [!WARNING]
Expand Down Expand Up @@ -77,25 +77,29 @@ func runQuickstart(count *int) func(cmd *cli.Command, args []string) error {

Will get you the following:

![quickstart](./docs/img/quickstart.gif)
![quickstart](https://github.com/FollowTheProcess/cli/raw/main/docs/img/quickstart.gif)

> [!TIP]
> See more examples under [`./examples`](https://github.com/FollowTheProcess/cli/tree/main/examples)

### Core Principles

#### 😱 Well behaved libraries don't panic

`cli` validates heavily and returns errors for you to handle. By contrast [spf13/cobra] (and [spf13/pflag]) panic in a number of conditions including:

- Duplicate subcommand being added
- Duplicate subcommand added
- Command adding itself as a subcommand
- Duplicate flag added
- Invalid shorthand flag letter

The design of `cli` is such that commands are instantiated with `cli.New` and a number of [functional options]. These options are in charge of configuring your command and each will perform validation prior to applying the setting.

These errors are joined and bubbled up to you in one go via `cli.New` so you don't have to play error whack-a-mole.
These errors are joined and bubbled up to you in one go via `cli.New` so you don't have to play error whack-a-mole, and more importantly your application won't panic!

#### 🧘🏻 Keep it Simple

`cli` has a tiny public interface and gives you only what you need to build amazing CLI apps, no more confusing options and hundreds of struct fields.
`cli` has an intentionally tiny public interface and gives you only what you need to build amazing CLI apps, no more confusing options and hundreds of struct fields.

There is one and only one way to do things (and that is *usually* to use an option in `cli.New`)

Expand All @@ -112,7 +116,7 @@ var force bool
cli.New("demo", cli.Flag(&force, "force", 'f', false, "Force something"))
```

Note the type `bool` is inferred by `cli.Flag`. No more `flag.BoolStringSliceVarP` 🎉
Note the type `bool` is inferred by `cli.Flag`. This will work with any type allowed by the `Flaggable` generic constraint so you'll get compile time feedback if you've got it wrong. No more `flag.BoolStringSliceVarP` 🎉

#### 🥹 A Beautiful API

Expand All @@ -134,7 +138,7 @@ cmd, err := cli.New(

#### 🔐 Immutable State

Typically, these sorts of things are implemented with a big struct with lots of fields. `cli` is no different in this regard.
Typically, commands are implemented as a big struct with lots of fields. `cli` is no different in this regard.

What *is* different though is that this large struct can **only** be configured with `cli.New`. Once you've built your command, it can't be modified.

Expand All @@ -149,7 +153,7 @@ Consider the following example of a bad shorthand value:
```go
var delete bool

// Note: bad shorthand, it's two letters
// Note: "de" is a bad shorthand, it's two letters
cli.New("demo", cli.Flag(&delete, "delete", "de", false, "Delete something"))
```

Expand All @@ -169,12 +173,6 @@ var delete bool
cli.New("demo", cli.Flag(&delete, "delete", cli.NoShortHand, false, "Delete something"))
```

### Credits

This package was created with [copier] and the [FollowTheProcess/go_copier] project template.

[copier]: https://copier.readthedocs.io/en/stable/
[FollowTheProcess/go_copier]: https://github.com/FollowTheProcess/go_copier
[spf13/cobra]: https://github.com/spf13/cobra
[spf13/pflag]: https://github.com/spf13/pflag
[urfave/cli]: https://github.com/urfave/cli
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Things I want to do to make this library as good as it can be and a better, simp
- [ ] Figure out if/how we support setting custom flag types e.g. implement `Value`. Try this out in an example, does it work with `Flaggable`, how do we make it work?
- [x] How to handle conflicting flags e.g. `--version` has a short of `-v`, what if we want `-v` to mean verbosity, should be an error if there's a conflicting flag
- [ ] Cleverly look at what `cli.Allow` is set to to dynamically render the usage info
- [ ] Use the default values in the flag usages somehow, maybe if the default is not the zero value for the type?
- [ ] Add example tests for some of the public functions

## Ideas

Expand Down
8 changes: 4 additions & 4 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,20 @@
return cmd.run(cmd, argsWithoutFlags)
}

// This basically only happens when we have subcommands defined but pass no args to the root command
// in which case we'll just show the help text and error
// The only way we get here is if the command has subcommands defined but got no arguments given to it
// so just show the usage and error
if err := defaultHelp(cmd); err != nil {
return err
}
return errors.New("invalid arguments")
return fmt.Errorf("command %q expected arguments (subcommands) but got none", cmd.name)
}

// Flags returns the set of flags for the command.
func (c *Command) flagSet() *flag.Set {
if c == nil {
// Only thing to do really, slightly more helpful than a generic
// nil pointer dereference
panic("Flags called on a nil Command")
panic("flagSet called on a nil Command")

Check warning on line 257 in command.go

View check run for this annotation

Codecov / codecov/patch

command.go#L257

Added line #L257 was not covered by tests
}
if c.flags == nil {
return flag.NewSet()
Expand Down
17 changes: 17 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Examples

This directory contains a bunch of example programs built with `cli` to show you how the library works and how you might implement common patterns.

- [Examples](#examples)
- [`./quickstart`](#quickstart)
- [`./subcommands`](#subcommands)

## `./quickstart`

Implements the [quickstart] command from the main project README

## `./subcommands`

A CLI with multiple subcommands, each with their own flags and expected arguments. Shows how to easily store parsed flag values in an options struct and pass them around your program.

[quickstart]: https://github.com/FollowTheProcess/cli#quickstart
Loading