flags: A command-line arguments parser using reflection in Go
Go is a great choice for programming command-line utilities.
It won’t be long however until you run into the standard
flag
parser.
It does its job, but in Go’s own peculiar way. As you may know, Go originated
from Plan9 (a research operating system best described as “UNIX, but
different”), and as a result many Go command-line programs inherit the look
and feel of a Plan9 program, which feels foreign and out of place on a
Linux/Mac/Windows system. As a fix, many Go developers implement their own
command-line arguments parser. Mind you, writing a decent, solid command-line
arguments parser module is trickier than you may think.
There are basically two kinds of command-line argument parsers:
- “getopt” style
- “builder pattern” style
The getopt
style parsers require coding a loop where you check which
argument was given. This means writing more explicit code, but at the same
time, they’re very flexible, and you can make it behave exactly how you want.
Examples of a getopt
style parser are getopt()
and getopt_long()
in C,
and getopts
in bash.
The builder pattern
style parsers are mini-frameworks that require you to
learn to use the framework appropriately. They are easy for trivial options
parsing, and not-so-great when you need special behavior.
Examples of a builder pattern style parser are argparse
in Python, and
clap
in Rust.
The flag
parser in the Go standard library is a builder pattern style parser,
one that allows you to tie command-line options to variables programmatically.
While a nice feature, surely Go can do much better than this.
Go has two exceptional features that enable us to implement a much more sophisticated command-line arguments parser: tagged structs and reflection.
Declaring flags in a tagged struct
Tagged structs are a way of adding metadata to struct fields. Reflection is a way of inspecting the innards of the program at runtime, and manipulating any values therein.
This allows us to write a “declarative” argument parser, like so:
import "github.com/walterdejong/flags"
type Options struct {
Help bool `flags:"-h, --help"`
Quiet bool `flags:"-q, --quiet suppress output"`
Verbose int `flags:"-v, --verbose be more verbose (may be given multiple times)"`
Num int `flags:"-n, --num=NUMBER specify number"`
Unsigned uint `flags:"-u, --unsigned=NUMBER specify number >= 0"`
File string `flags:"-f, --file=FILE specify filename"`
}
We declare a tagged struct, containing metadata that essentially is the
help
message for the program’s options.
Now, calling the flags.Parse()
function will do the rest for us. How?
By virtue of reflection, the flags
module is able to fill in the struct
fields.
The magic of reflection
The first step of magic is retrieving the tags from the struct fields.
Mind that Go has strict typing, so we must use type any
to pass
(a pointer to) a user-defined tagged struct.
func inspectTag(taggedStructP any) {
s := reflect.ValueOf(taggedStructP).Elem()
typeOfT := s.Type()
for i := 0; i < typeOfT.NumField(); i++ {
field := typeOfT.Field(i)
tag := field.Tag.Get("flags")
fieldIndex := i
// ...
What this snippet of code does is look at the type of the passed value (which is a struct), and then proceeds to inspect each field in the struct, getting the tags. The tag is a string, containing the text we gave above.
The fieldIndex
is the index of the field in the struct. We are going
to need this later, when setting values.
We can also inspect the type of the field:
kind := field.Type.Kind()
// typecheck the field
switch kind {
case reflect.Bool, reflect.Int, reflect.Uint, reflect.String:
// pass
default:
panic(fmt.Sprintf("unable to handle type %v (not implemented)", kind))
}
For command-line arguments we only support these types, otherwise it’s bad
use of the flags
module, in which case we abort the program.
The flags
module continues to parse the command-line arguments.
Reflection is again used to put the option argument values in
the struct fields. So suppose we want to have an optional argument
$ prog --file=foo.txt
then we have in our Options
struct a string field:
File string `flags:"-f, --file=FILE specify filename"`
Setting the value of a struct field is just that: select the right field
(by using the field index) and issue the appropriate setter function.
The type of the reflected field is reflect.Value
. Like above, the field
may represent any kind of type. In code:
setValue(s.Field(fieldIndex), svalue)
// ...
func setValue(field reflect.Value, svalue string) error {
switch field.Kind() {
case reflect.String:
field.SetString(svalue)
case reflect.Int:
// ...
The optional argument value we have is svalue string
, so if we have
an option of type int
we need to parse the value:
case reflect.Int:
i, err := strconv.ParseInt(svalue, 10, 64)
if err != nil {
return err
}
field.SetInt(i)
Similarly for other types of option arguments.
On the app side
Now that we know how the internals of the flags
module work, it’s time to
return to the application side of things: parsing the command-line arguments
from an applications developer’s perspective.
Doing so is laughably easy. Remember the tagged struct we declared above?
We need to instantiate it and pass it into the Parse()
function.
Upon return, the struct fields will be filled in. We also get back any
remaining arguments that may have been on the command-line (think: a list
of filenames).
opts := Options{}
args, err := flags.Parse(os.Args, &opts)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(2)
}
fmt.Printf("opts == %#v\n", opts)
fmt.Printf("args == %#v\n", args)
Handling any program logic according to the given option is up to the
programmer. This goes even for printing the help
information;
doing it this way allows you to easily customize the help output.
if opts.Help {
fmt.Println("usage: example [options] [args ...]")
flags.PrintHelp(&opts)
os.Exit(1)
}
We have come to the end of this post, which may have been quite hard
to follow to those unaware of Go and reflection. The flags
module is
a novel take on command-line argument parsing, one that demonstrates
the technical prowess of Go.
Only one statement left to make;
import "github.com/walterdejong/flags"