Why did I build my own when there’s a hundred of these already? Because I don’t like how most of the others work. Fairy 🧚 only uses Go’s flag
package underneath. There’s no custom flag parsing involved, but it can do some things Go’s standard library package doesn’t provide.
I quite like Go’s flag package. I don’t care that it doesn’t do GNU-style opts, that it’s not perfectly docopt etc. It fits my needs 95% of the time and unless you need a hyperscale Cloud Native CNCF approved CLI it probably is enough for just about any project.
However, using Go’s flag package is not super fun. Fairy adds a sprinkling of magic on top of it to make it much easier to use and much more convenient to build more complex things with. But about 85% of fairy’s functionality is backed by the standard library’s flag.FlagSet
.
What does fairy give you?
Quality of life magic 🪄:
- Short and long flags for a single option. 🤯
- Support slices of any type. 😲
- Bind a custom type to a flag. 🥵
- Subcommands galore. 🤩
Magic flags
It’s really not magic. It’s registering the flag twice, with the same point-to-value. The PrintDefaults function is a modified version of the stdlib one that groups these flags together. That’s it.
Magic types
There are two special, generic, types:
fairy/flag.E[T]
fairy/flag.S[T]
These represent a flag of a single E
lement, or a S
lice of type T
. They implement stdlib’s flag.Value
and dynamically look up the parsing function for T
in their Set
method. There’s actually very little magic. Just a bit of convenience. They’re registered using flag.Var
.
Parsing functions for all common types like all ints, uints and floats are automatically registered with the package. Bool, string, time.Duration
and netip.Prefix
as well. For anything else you can register your own parsing functions using fairy/flag.ParserFor
.
You don’t really need fairy/flag.E[time.Duration]
or fairy/flag.E[int]
as stdlib supports those types natively. But you’ll need fairy/flag.E[int8]
or fairy/flag.E[netip.Prefix]
. For slices you’ll always need fairy/flag.S
since stdlib doesn’t support that at all out of the box.
Of course, if you want, you can implement flag.Value
the old fashioned way and bind your custom types yourself. Or register a parsing function with fairy and not bother with any of that. Up to you.
Magic structs
Fairy also does struct binding. Create a command and Bind
it to the (pointer of a) struct of your chosing. Exported fields automatically become flags, and their values get updated when a flag is passed on the CLI.
How fields get their short and long flag names can be fully controlled using struct tags. You can also ignore exported fields using a tag. There is also a struct tag for setting the usage text.
Magic commands
Everything is a command, including the root command. There’s a main function that gets called for the name of the command itself, and any number of command functions for subcommands. You can chain them as deep as you want. Turtles all the way down.
Fairy automatically handles looking at the arguments and determining if a subcommand exists, or if it should invoke the main command instead.
Magic convenience
Additionally, there’s built-in functionality to:
- Provide a
-v/--version
flag. - Install
help
andversion
subcommands.
Both the version flag as well as the help and version commands are implemented just like any other flag or subcommand. It’s not a special flag or special subcommands, and you can override their behaviour.
What doesn’t it do
Fairy doesn’t bind flags from environment variables, and it doesn’t read and parse configuration files and bind them to flags for you. You really don’t need to support 4 configuration formats in your app either. One is fine. Configuration files are read by machines far more often than they’re written by people. Folks can deal.
At the risk of potentially pissing off the internet: I really don’t like the religious adherence to 12-factor. Security-wise, there are plenty of problems with secrets in environment variables. They’re no better or worse than files, and you should understand what risks each has and mitigate them appropriately. For everything else, why give up nicely typed configuration files for stringly typed environment variables? It’s not this hard to mount a file in a container.
Environment variables are useful as configuration knobs for libraries. But they’re not that great for end user programs. CLI flags have this problem too, but at least there’s a native mechanism for setting a flag to more than one value. Have a --config
flag and consider having some built-in paths you automatically read, like XDG_CONFIG/<app>/config.json
. I shouldn’t have to set fifteen environment variables or flags whenever I want to invoke a CLI.