CLI App with Subcommands in Go
In this article we are going to take a look at parsing command-line arguments
with the support of subcommands using the standard library in Go. Our example
application, called futil
, is a file utility which allows us to copy and
delete files. The details are not really important here, as we just need a
concrete example of a CLI tool with multiple commands.
futil [flags] <command> [command flags]
The general structure of our command-line tool is as follows:
- we can provide global flags used for every command
- specify one command we want to run
- each command can have specific command flags
Here are some examples of commands we might want to run.
futil -help
futil -debug copy -overwrite input.txt output.txt
futil delete input.txt
You can probably guess what the commands above are supposed to do, but that’s not too important. We are not going to implement the actual command logic. Instead, we will focus on how to parse these commands using the standard library.
The flag package
The important data structure we care about when parsing command-line flags is
the flag.FlagSet
type. It allows us to
specify various types of command-line flags (int, string, etc.), define a
help/usage message, and provides easy access to all the command arguments that
are not flags.
Creating a new FlagSet
is easy:
flagSet := flag.NewFlagSet("flagSetName", flag.ExitOnError)
We can then use functions like flagSet.BoolVar
to define a flag name and a
variable where the parsed flag value should be stored. Here an example using a
string flag with an empty string as default value.
var name string
flagSet.StringVar(&name, "name", "", "usage string")
Once you defined the flags, you can call flagSet.Parse()
. This will take a
list of strings as input (the command-line arguments) and parses the arguments
one by one until it finds a value which is not a flag. If an unknown flag is
encountered during parsing, the parse function will either return an error or
exit the program if you specify flag.ExitOnError
like we did in the example
above.
args := []string{"-name", "nameValue", "sub_command", "-subFlag"}
flagSet.Parse(args)
This will parse the -name
flag and the corresponding value nameValue
and store
nameValue
into the name
variable. Calling flagSet.Args()
will now return
the remaining arguments ["sub_command", "-subFlag"]
. To parse the flags for
our subcommand, we repeat the same process: create a new FlagSet
and parse
flagSet.Args()[1:]
from our previous flagSet
.
Example CLI App
By now you should have a rough understanding about FlagSet
s and how they work. Now
let’s see how we are going to implement the futil
tool described above.
import (
"flag"
"fmt"
"os"
"slices"
)
func main() {
// Just an example for parsing a flag before any subcommand, we are not
// using the value of this flag anywhere in the example program
var showDebugLog bool
flag.BoolVar(&showDebugLog, "debug", false, "print debug messages")
flag.Usage = usage // see below
flag.Parse()
// user needs to provide a subcommand
if len(flag.Args()) < 1 {
flag.Usage()
os.Exit(1)
}
subCmd := flag.Arg(0)
subCmdArgs := flag.Args()[1:]
fmt.Println(subCmd, subCmdArgs)
}
Instead of creating a new FlagSet
to parse the arguments, we are using the
default FlagSet
provided by the standard library. Functions like
flag.BoolVar
, flag.Usage
will manipulate this default FlagSet
, which is
defined as follows in the flag
package of the standard library.
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
flag.Parse
will then simply call CommandLine.Parse(os.Args[1:])
, which is
exactly what we need in the first step to parse the global flags.
The usage function is pretty simple. Just a bunch of fmt.Fprint...
calls.
func usage() {
intro := `futil is a simple file manipulation program.
Usage:
futil [flags] <command> [command flags]`
fmt.Fprintln(os.Stderr, intro)
fmt.Fprintln(os.Stderr, "\nCommands:")
// TODO: print commands help
fmt.Fprintln(os.Stderr, "\nFlags:")
// Prints a help string for each flag we defined earlier using
// flag.BoolVar (and related functions)
flag.PrintDefaults()
fmt.Fprintln(os.Stderr)
fmt.Fprintf(os.Stderr, "Run `futil <command> -h` to get help for a specific command\n\n")
}
Running the program using go run . -h
will now print the following help
message.
futil is a simple file manipulation program.
Usage:
futil [flags] <command> [command flags]
Commands:
Flags:
-debug
print debug messages
Run `futil <command> -h` to get help for a specific command
Note that we did not implement parsing the -h flag. That is automatically
handled by the flag.Parse
function. Using the -h
or -help
flag will stop
executing our program and call flag.Usage
, which we have overwritten to print
our custom message.
At this point we have access to the subcommand string inside the subCmd
variable. The simplest way to handle the subcommand is probably just a switch
on the subCmd
and calling a different function for each subcommand. But I not
only want to run each subcommand, but also define some help strings for each
command which are going to be used in the usage
function. So defining a
simple Command struct will simplify things a bit.
type Command struct {
Name string
Help string
Run func(args []string) error
}
var commands = []Command{
{Name: "copy", Help: "Copy a file", Run: copyFileCmd},
{Name: "delete", Help: "Delete a file", Run: deleteFileCmd},
{Name: "help", Help: "Print this help", Run: printHelpCmd},
}
func printHelpCmd(_ []string) error {
flag.Usage()
return nil
}
func copyFileCmd(args []string) error {...}
func deleteFileCmd(args []string) error {...}
A command is very simple. It has a name, a help message and a run function
which implements the command logic. The printHelpCmd
is also very simple, it
just calls the flag.Usage
function to print the custom usage message we
defined earlier. Before we look at one of the subcommands, let’s update our
usage
function to also print the help messages for each subcommand.
func usage() {
// ...
fmt.Fprintln(os.Stderr, "\nCommands:")
for _, cmd := range commands {
fmt.Fprintf(os.Stderr, " %-8s %s\n", cmd.Name, cmd.Help)
}
// ...
}
Let’s now look at the copyFileCmd
to see how we implement one of the
subcommands.
func copyFileCmd(args []string) error {
var overwrite bool
flagSet := flag.NewFlagSet("copy", flag.ExitOnError)
flagSet.BoolVar(&overwrite, "overwrite", false,
"overwrite the target file if it exists")
flagSet.Usage = func() {
fmt.Fprintln(os.Stderr, `Copy a file.
Usage:
futil copy [flags] SOURCE DESTINATION
Flags:`)
flagSet.PrintDefaults()
fmt.Fprintln(os.Stderr)
}
flagSet.Parse(args)
// actual copy implementation goes here
fmt.Println("Copy", flagSet.Args())
return nil
}
As you can see, we follow the same pattern as we did in our main function. The
only difference is, that we use a custom FlagSet
. We create the flagSet
,
bind a boolean variable to the -overwrite
flag, overwrite the Usage
function
to print a custom message and then finally call flagSet.Parse
. The
deleteFileCmd
follows the exact same pattern, so I’m not going to provide the
implementation here.
Now all that is left to do is to actually call the correct subcommand from our main function. So we are going to update our main function as follows:
func main() {
// ...
subCmd := flag.Arg(0)
subCmdArgs := flag.Args()[1:]
runCommand(subCmd, subCmdArgs)
}
func runCommand(name string, args []string) {
cmdIdx := slices.IndexFunc(commands, func(cmd Command) bool {
return cmd.Name == name
})
if cmdIdx < 0 {
fmt.Fprintf(os.Stderr, "command \"%s\" not found\n\n", name)
flag.Usage()
os.Exit(1)
}
if err := commands[cmdIdx].Run(args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s", err.Error())
os.Exit(1)
}
}
That’s it. We now have a CLI app including global flags, subcommands and
command specific flags, all of which are documented using the Usage
functions.
Run go run . -h
or go run . help
to show the help message including a
listing of all subcommands. Run go run . copy -h
to show the help message of
the copy
command.
Closing thoughts
Hopefully you saw how easy it is the parse command-line flags and arguments using Go. A bit verbose maybe but still pretty straightforward. And writing an app specific abstraction layer to hide the verbosity behind a few functions and data structures is also quite easy. But if you don’t feel like writing your own abstraction layer, maybe checkout one of the 3rd party libraries for parsing command line arguments like cobra or KONG. I’ve used both in the past and was quite happy with them.