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.ExitOnErroroption 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.Parsefunction, theFlagSet.Parsemethod 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.VisitAlliterates over every registered global option -
FlagSet.Varregisters 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 FlagSets 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.