This article walks through usage of the Go standard library’s flag package to implement support for subcommands in a program.
1. Requirements
Usage of applications with subcommands typically takes the following form.
command [global options] subcommand [subcommand options] [subcommand arguments]
Where,
- global options
-
all subcommands share these
- subcommand options
-
specific to a subcommand
- subcommand argument
-
non-option arguments specific to a subcommand
For example, consider the command:
git --no-pager branch -m foo mybranch
This can be broken down into the following components:
To implement subcommands with the flag package, we need to do the following:
2. Parse global options
Register global options with the flag package and parse them with
flag.Parse
.
This looks like regular usage of the flag package.
var (
_noPager = flag.Bool("no-pager", false, "do not pipe into a pager")
_workTree = flag.String("work-tree", "", "path to the working tree")
)
func main() {
flag.Parse()
💡 Tip
|
Prefix unexported globals with _ to prevent
unintentional references or accidental shadowing to them.
|
3. Dispatch to subcommand
Retrieve the remaining arguments with flag.Args
.
The first of these is the subcommand name,
and the rest are the arguments and options for the subcommand — we will parse those in the subcommand implementation.
args := flag.Args()
if len(args) == 0 {
log.Fatal("Please specify a subcommand.")
}
cmd, args := args[0], args[1:] (1)
-
Given the command invocation
mygit branch -m foo mybranch
, we get,cmd = "branch" args = ["-m", "foo", "mybranch"]
Match the subcommand by name and invoke the corresponding implementation with
the remaining arguments in args
.
switch cmd {
case "branch":
branch(args)
case "checkout":
checkout(args)
default:
log.Fatalf("Unrecognized command %q. "+
"Command must be one of: branch, checkout", cmd)
}
💡 Tip
|
Use %q for strings inside error messages so that they stand out from the rest of the error message. |
4. Parse subcommand options
For each top-level flag parsing function in the flag package,
the flag.FlagSet
type provides a similar method.
|
|
Build a FlagSet
for each subcommand and use these methods to parse the
subcommand options and retrieve the remaining arguments.
func branch(args []string) {
flag := flag.NewFlagSet("mygit branch", flag.ExitOnError) (1)
var (
del = flag.Bool("d", false, "delete branch")
move = flag.String("m", "", "move/rename a branch")
list = flag.Bool("l", false, "list branch names")
)
flag.Parse(args) (2)
args = flag.Args()
-
The
flag.ExitOnError
option specifies that if there is a flag parsing error, the library should print a helpful message and exit. This matches the behavior offlag.CommandLine
. -
Unlike the top-level
flag.Parse
function, theFlagSet.Parse
method expects the command line arguments to be passed in.
That’s it! The branch
subcommand above has options that are not shared
with other subcommands.
💡 Tip
|
Shadow the flag package In this example,
This is desirable. It ensures that you do not unintentionally use the top-level flag functions instead of methods on the flag set. For example,
By shadowing |
5. Propagate global options
Often, but not always, applications support providing global options next to the subcommand options. For example,
$ git --work-tree=foo branch -l
* main
$ git branch -l --work-tree=foo
error: unknown option `work-tree=foo'
Oops! Git does not actually support this. But this post chose Git as an example, so we’ll implement support anyway.
Like Git, our program will currently reject global options passed to subcommands.
$ mygit --work-tree=foo branch [..]
# SUCCESS
$ mygit branch --work-tree=foo [..]
# ERROR: flag provided but not defined: --work-tree
Unlike Git, we will support this.
The top-level flag functions like flag.Bool
and flag.String
register
flags against the flag.CommandLine
flag set.
This flag set holds the global options and is completely disconnected from
the flag set that we constructed for the mygit branch
command.
We can use two tools to solve this problem:
-
flag.VisitAll
iterates over every registered global option -
FlagSet.Var
registers flags with a flag set
Add the following function to your code:
func registerGlobalFlags(fset *flag.FlagSet) {
flag.VisitAll(func(f *flag.Flag) { (1)
fset.Var(f.Value, f.Name, f.Usage) (2)
})
}
-
For every global option…
-
…register a copy with the provided FlagSet
Use the new function in branch
to register the global options with the new
flag set.
list = flag.Bool("l", false, "list branch names")
)
registerGlobalFlags(flag)
flag.Parse(args)
args = flag.Args()
The mygit branch
subcommand is now aware of the global options and will
update the variables on parse.
$ mygit --work-tree=foo branch [..]
# SUCCESS
$ mygit branch --work-tree=foo [..]
# SUCCESS
mygit
is now more flexible than real Git — minus the actual functionality of
Git, but that’s a discussion for another time.
5.1. The flag.Value
interface
We glazed over the mechanics behind the FlagSet.Var
method above.
This section attempts to cover that in more detail.
The FlagSet.Var
method (and the corresponding flag.Var
function on flag.CommandLine
) have the following signatures:
func (f *FlagSet) Var(value Value, name string, usage string)
func Var(value Value, name string, usage string)
Both accept an object implementing the flag.Value
interface,
defined as follows.
type Value interface {
Set(string) error (1)
String() string (2)
}
-
receives a value from the command line
-
reports the current value as a string
📢 Important
|
This is the backbone of the flag package. Every other flag is implemented in terms of this. |
For example, when you call flag.Int
, it ultimately builds an object
that implements this interface around an int
.
This looks roughly like the following,
type intValue int
func (i *intValue) Set(s string) error {
v, err := strconv.ParseInt(s, 0, strconv.IntSize)
if err != nil {
// ...
}
*i = intValue(v)
return err
}
// String implementation omitted for brevity.
In the previous section, we exploited the fact that all
flags are implemented in terms of flag.Value
implementations like
intValue
above.
We got access to these Value
objects with flag.VisitAll
,
and re-registered these flags against a different FlagSet
.
Both FlagSet
s will feed into the same Value
object.
Custom Value
objects
You can implement custom flags by implementing this interface yourself
and supplying the result to the Var
method.
For example, if you want a flag that accepts a comma-separated list of values,
you can implement it around a []string
, using strings.Split to turn
the flag value into a slice.
type stringList []string
func (sl *stringList) Set(value string) error {
for _, s := range strings.Split(value, ",") {
*sl = append(*sl, s)
}
return nil
}
// String implementation omitted for brevity.
// Register the flag like so,
var items stringList
flag.Var(&items, "o", "comma-separated list of options")
// "-o foo,bar,baz" turns into []string{"foo", "bar", "baz"}
This, too, will work as expected with our Value
re-registration trick above.
6. Conclusion
The standard library’s flag package is small and flexible. Explore it and see if it satisfies your needs before introducing third-party dependencies.
A future post may cover practices around structuring your usage of the package for better testing.