-
Notifications
You must be signed in to change notification settings - Fork 1
Introduction
Many Go programmers are now familiar with the idea of declaring CLI functions through structs.
The fundamental idea is that you can declare a struct
embedding various flags, subcommands
and positional arguments, themselves declaring other subcommands, etc, etc.
That way, your struct is a tree that is similar to a CLI command tree.
On top of this, since Go allows to tag struct fields (commonly used by encoding libraries), the idea of declaring the CLI interface specs through struct tags has the advantage of using that same struct tree, and to have next to each field of interest, its various CLI specs.
But there are some caveats to this method:
- Struct tags are not typed, they are pure strings.
- The longer the tag, the more your code flees to the right, which is not great in all occasions.
- Struct tags are not great as soon as you want to add long strings, a detailed usage for instance.
- So as soon as you need quite specific features to your CLI, struct tags can't help you, and you need another API.
The most widely used CLI library is cobra. It is now a battle-tested library, with a large feature set, sane defaults, and pretty much every feature you would need for CLI programs.
Not leveraging this library, or trying to reinvent it, is a huge mistake for several reasons:
- The feature set is large, and battle-tested.
- The community is very active.
- It offers a conventional way of declaring, modifying and using commands or their components.
- The API is uniform, from the root parser (entrypoint) all the way to the deepest command node.
For those reasons, the flags library parses native struct
types into cobra command trees.
There are several Go validation libraries. Some of them are not maintained. go-validator is a good compromise between its range of possible validations, its syntax, and the current support development/number of open issues.
- Minimizing CLI development time: The main idea of the flags library is to maximize the functionality of a CLI program, while allowing developers to spend as much time as possible focusing on the program itself, not on how to interface it correctly with a shell.
- Complete CLI tag specification: The flags library offers a complete set of tags in order to allow developers to declare all of the components of a CLI command through a struct tree (subcommands, flags, positionals), and while doing so, to offer the most precise and versatile way of doing so (positionals are a good example of where this is achieved).
-
Leveraging proven libraries: Parsing structs into
cobra.Commands
enables to have battle-tested CLI programs out-of-the-box, and to continually benefit from improvements in the cobra library. - Mixing APIs and command sources: Since all generated commands/components are cobra commands, the flags library enables to use different command sets from different sources, and to mix them arbitrarily with the same result: uniform and reliable use/execution of CLI applications, from simplest to most complex ones.
- Providing comfort-of-life features for free: Building on the all the above points, the library aims to provide as many comfort-of-life features to the CLI programs it generates, at a minimum cost, for the largest number of shell environments.
All of the above explanations can somewhat boil down to 2 principles and simple code snippets:
1) The following type is a valid command. Now matter where, no matter how.
Nevertheless this precise command has, for now, no arguments nor options,
and its implementation is empty, it does nothing. Better, the Command
could even not have the Execute
method, and could still be parsed as
a cobra command.
type Command struct {}
func (c *Command) Execute(args []string) (err error) {
return
}
2) Continuing with the Command
type, the second principle driving everything
in this library is the following: the command below provides already sufficient
information for a parser to apply a command line string's contents onto it.
type Command struct {
Args struct {
Filename string `description:"filename to get"`
Otherfiles []string `description:"optional list of other files"`
}`positional-args:"yes"`
Options struct {
TargetURL string `short:"t" long:"target"`
Retries int64 `short:"r" long:"retries" default:"100"`
MaxHostErrors map[string]int64 `short:"e" description:"my description"`
} `group:"command options"`
Subcommand struct {
Args MyArguments `positional-args:"yes"`
Opts SubOpts `group:"sub options"`
} `command:"yes" description:"a perfectly working subcommand"`
}