Most CLI applications require bits of configuration when being invoked. In Go,
this can be done natively using the flag package. It proposes a simple
interface for defining argument flags sets, but may feel like it is lacking some
functionality. Over time, a plethora of libraries were developed; spf13/cobra
and urfave/cli being probably the most used ones (both have tens of thousands
stars on GitHub).
While they offer lots of features out of the box, I often found them quite complicated and I wanted to understand how they worked and tried to get the bare minimum of their functionality with as little code as possible.
In this post, we will have an overview of how Go’s flag works and see some
advanced uses of it. We will then try to implement parsing of complex values
such as comma-separated arguments or slices.
The basics of flag parsing
The flag package looks a bit overwhelming but the basic usage is actually not
that complicated. Most of the time, users will only need to declare flags and
make a call to do the parsing:
func main() {
path := flag.String("config", "", "configuration file `path`")
flag.Parse()
fmt.Println(*path)
}
The function above defines a config flag that will store the parsed value in a
variable pointed by path. Note the backticks around `path` in the usage
string; it will be used as a placeholder when printing Usage:
$ go run main.go -h
Usage:
-config path
configuration file path
Flags may be defined by calling functions named after the type of data that needs to be parsed. Basic types are usually available. For instance:
// These functions defines a named flag with a default value and usage string.
// The returned value is the address of a variable storing the parsed value.
func (f *FlagSet) Bool(name string, value bool, usage string) *bool
func (f *FlagSet) String(name string, value string, usage string) *string
func (f *FlagSet) Int(name string, value int, usage string) *int
These also have a Var variant that will store the value in a pre-declared
variable. An usage example would be the configuration of a package variable:
package mypkg
var Foo = "foo"
func main() {
flag.StringVar(&mypkg.Foo, "foo", mypkg.Foo, "configure mypkg.Foo")
flag.Parse()
}
$ go run main.go -h
Usage:
-foo string
configure mypkg.Foo (default "foo")
Finally, the Func flag will call a function with the flag value as a parameter.
It however does not allow setting a default value.
The BoolFunc flag works similarly, but does not require a value to use the flag.
The function will then be called as if the flag was set as -name=true.
func main() {
flag.BoolFunc("version", "print version and exit", func(string) error {
fmt.Println("x.y.z")
os.Exit(0)
return nil
})
flag.Parse()
}
Parsing user-defined values
Parsing basic types is cool but you will probably need to parse more complex
objects. This may be done using the Func flag:
func main() {
var person struct{ firstname, lastname string }
flag.Func("person", "configure person (`firstname:lastname`)", func(s string) error {
var cut bool
person.firstname, person.lastname, cut = strings.Cut(s, ":")
if !cut {
return errors.New("invalid format")
}
return nil
})
flag.Parse()
}
This can feel quite hacky/messy, so I would not use this unless the configured object is limited in scope and has a simple parsing function. And this does not fully support default values as well.
In our example, the proper way would be to define a person type implementing
the flag.Value interface and use the flag.Var function:
type person struct{ firstname, lastname string }
// String implements [flag.Value] and [fmt.Stringer]
// This method requires a proper implementation, otherwise the [flag]
// package may fail to determine whether a default value was set
func (p *person) String() string {
if p == nil { // The [flag] package may call the method on a zero-valued receiver
return ":"
}
return fmt.Sprintf("%s:%s", p.firstname, p.lastname)
}
// Set implements [flag.Value]
func (p *person) Set(s string) (err error) {
var cut bool
p.firstname, p.lastname, cut = strings.Cut(s, ":")
if !cut {
err = errors.New("invalid format")
}
return
}
func main() {
person := person{"john", "doe"}
flag.Var(&person, "person", "configure person (`firstname:lastname`)")
flag.Parse()
}
We can also implement the Set method to parse comma-separated lists of values:
type names []string
func (x *names) String() string {
if x == nil || len(*x) == 0 {
return ""
}
return fmt.Sprint(*x)
}
func (x *names) Set(s string) error {
*x = strings.Split(s, ",")
return nil
}
From here, I realized how difficult it was to make a library with an interface that is precise, easy to use but also flexible. I gave it a try but always ended up getting something either very opinionated or relied on non-native types.
Indeed, in the last snippet of code, we are splitting the flag value to store sub elements into a slice. This implies that the slice will contain only the elements of the last parsed flag, and this may or may not be what is needed. There is a lot of variants that can be though of:
// Parses semicolon separated values
func (x *names) Set(s string) error {
*x = strings.Split(s, ";")
return nil
}
// Parses whitespace separated values
func (x *names) Set(s string) error {
*x = strings.Fields(s)
return nil
}
// Parses whitespace separated values
// Values are appended over multiple flags
func (x *names) Set(s string) error {
*x = append(*x, strings.Fields(s)...)
return nil
}
// Values are appended over multiple flags as is
func (x *names) Set(s string) error {
*x = append(*x, s)
return nil
}