Parsing Command-Line Flags
The flag package's headline function is flag.Parse(), and every
tutorial example calls it the same way:
var verbose = flag.Bool("verbose", false, "enable verbose output")
flag.Parse() // parses os.Args, using the package-level flag.CommandLine set
This works fine for a main that runs once. It is close to useless for
testing, because flag.CommandLine is a single package-level variable,
defined once at import time. Call flag.Parse() a second time in the same
process — say, from a second test case trying different arguments — and
you're not parsing fresh flags; you're re-parsing into ones already
registered, which either panics ("flag redefined") or silently reuses
whatever the first call already set. A table-driven test, the pattern this
book has used for almost every exercise, is exactly the shape flag.Parse()
can't support.
flag.NewFlagSet: the same API, not shared
func ParseArgs(args []string) (verbose bool, name string, err error) {
fs := flag.NewFlagSet("prog", flag.ContinueOnError)
fs.BoolVar(&verbose, "verbose", false, "enable verbose output")
fs.StringVar(&name, "name", "world", "who to greet")
err = fs.Parse(args)
return verbose, name, err
}
Every call to ParseArgs creates its own *flag.FlagSet — its own,
independent registry of flags, entirely unrelated to flag.CommandLine or
any other call to this same function. Nothing is shared, so nothing
conflicts, and this function can be called a thousand times in a loop with a
thousand different args slices, which is precisely what the hidden test
does. os.Args[1:] (the real command line) is just one possible value of
args — a normal slice, not a special global, so passing it in instead of
reading it inside the function is what makes this testable in the first
place.
flag.ContinueOnError matters too: the package-level flag.CommandLine
defaults to flag.ExitOnError, which calls os.Exit on a bad flag — fine
for a real CLI process, fatal for a test, which would just... stop, with no
failure message pointing at what went wrong. ContinueOnError returns the
parse error instead, so it becomes an ordinary Go error your test can
assert against.
Why this is worth a lesson on its own
It's tempting to treat "make it testable" as a nice-to-have you'd get to
later. For flag parsing specifically, it's not optional: flag.Parse()
against the global set is something you can call successfully exactly once
per process, ever. A function built around it doesn't have a testing
gap you can patch in later — it has to be built the untestable way or the
testable way from the first line, and NewFlagSet is the entire difference
between the two.
Try it
Implement ParseArgs(args []string) (verbose bool, name string, err error)
using a fresh flag.FlagSet per call, with flag.ContinueOnError.
Practice
1. Parse Flags Without a Global
IntermediateImplement
ParseArgs(args []string) (verbose bool, name string, err error).-verboseis a bool flag, defaultfalse.-nameis a string flag, default"world".- Use a fresh
flag.FlagSetper call (not the package-levelflagfunctions) withflag.ContinueOnError, so this can be called repeatedly with differentargsin the same process. - Return whatever error
Parseproduces, unwrapped.
package main import "fmt" // ParseArgs parses -verbose and -name from args using a fresh FlagSet, so // it can be called repeatedly with different args in the same process. func ParseArgs(args []string) (verbose bool, name string, err error) { // TODO: implement return false, "", fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.