Choosing a Go CLI Library
7 March 2021
Over the years I’ve used many golang CLI modules. Some have just parsed flags, some have been whole “frameworks”; halfway to a gokit or go micro. For a long time I was a big fan of the classic cobra/viper combo, and I think they still have merit.
However lately I’ve written a few little utilities that have needed to take args from flags, but don’t want a whole framework trying to wrestle control from them. In particular, I didn’t want Inversion of Control problems.
So I set about choosing a new favourite CLI library. My criteria were:
- Testable (can I at least pass in a dummy
os.Args
?) - Type safe (so far as go is)
- Auto-generation of usage
- Auto-generation of bash completion
- Consume flags and their args, leaving positional args
- Ideally, some support for verbs / actions / sub-commands
Results
Well, I didn’t even test most of them. The first one I tried was go-flags and it rocks. I was satisficed, so I stopped.
Library | Testable | Type Safe | Usage | Completion | Positionals | Verbs |
---|---|---|---|---|---|---|
pkg/flag | ? | ? | ? | ? | ? | ? |
cobra | ? | ? | ? | ? | ? | ? |
urfave/cli/v2 | ? | ? | ? | ? | ? | ? |
uber? | ? | ? | ? | ? | ? | ? |
kingpin | ? | ? | ? | ? | ? | ? |
mow.cli | ? | ? | ? | ? | ? | ? |
argparse | ? | ? | ? | ? | ? | ? |
mitchelh/cli | ? | ? | ? | ? | ? | ? |
go-flags | yes | yes | yes | yes | yes | yes |
Example
// Declare a variable to take the parsed flags.
// You can make a type, or just use an anonymous struct on the fly like this.
var opts struct {
Type bool `short:"t" long:"type" description:"Print the type"`
TypeSymbol bool `short:"T" long:"type-symbol" description:"..."`
Unit bool `short:"u" long:"unit" description:"Print the unit"`
Name bool `short:"n" long:"name" description:"Print the name"`
}
// Parse the command line.
// flags.Parse() implicity uses os.Args, and drops the 0th element.
args, err := flags.Parse(&opts)
if err != nil {
panic(err)
}
// args now contains anything that didn't match a specification in opts.
fmt.Println("Positional args: ", args)