How do you implement git-style subcommands in Go with flag.NewFlagSet, each owning its own flags?
answer
- one parser per verb, not one global one
- the argument vector is split before parsing
- the first token chooses the flag set
- flag.NewFlagSet, then Parse on os.Args[2:]
basics
~10 sGive each subcommand its own *flag.FlagSet from flag.NewFlagSet, switch on os.Args[1] to choose one, and call that set's Parse on os.Args[2:]. Each set owns its own flag names, usage text and operands.
solid answer
~40 sGo's `flag` package has no built-in dispatcher, so you split the argument vector yourself. `os.Args[1]` is the subcommand name; you `switch` on it, create the matching `*flag.FlagSet` with `flag.NewFlagSet("add", flag.ContinueOnError)`, register that command's flags on it, and call `fs.Parse(os.Args[2:])`. Because each set is independent, two subcommands can define the same flag name with different meanings, and `fs.Args()` gives that command its own operands. I prefer `flag.ContinueOnError` over `flag.ExitOnError` because Parse then returns the error to me — including `flag.ErrHelp` when `-h` is asked for and no such flag is defined — so `main` decides what to print and the parsing is unit-testable. Set `fs.Usage` per command for its own help text, backed by `fs.PrintDefaults()`, and print the top-level synopsis when the first argument matches no command.
code
go · 22 linesfunc run(argv []string) error {
if len(argv) == 0 {
return errors.New("usage: tool <add|list> [flags] [args]")
}
switch argv[0] {
case "add":
fs := flag.NewFlagSet("add", flag.ContinueOnError)
force := fs.Bool("force", false, "overwrite existing entries")
if err := fs.Parse(argv[1:]); err != nil {
return err // flag.ErrHelp when -h was requested
}
return add(*force, fs.Args())
case "list":
fs := flag.NewFlagSet("list", flag.ContinueOnError)
long := fs.Bool("l", false, "one entry per line")
if err := fs.Parse(argv[1:]); err != nil {
return err
}
return list(*long)
}
return fmt.Errorf("unknown subcommand %q", argv[0])
}go deeper
Know that the flag package does not know about subcommands: you look at the first argument yourself and choose which parser runs. Be able to name flag.NewFlagSet.
Walk through the slicing — os.Args[1] selects the verb, os.Args[2:] goes to that set's Parse — and explain why each verb gets an independent set with its own flags and usage.
Justify ContinueOnError over ExitOnError in terms of testability and a single error-reporting path, and explain how you handle flag.ErrHelp and an unknown verb distinctly.
Own the contract: whether global flags come before or after the verb, how new subcommands get added without breaking scripts, and how help output stays consistent as the command surface grows.
## The package gives you sets, not a dispatcher A git-style command line — `tool add -force file`, `tool list -l` — has two levels: a verb, then the flags belonging to that verb. Go's `flag` package models the second level with `*flag.FlagSet` and leaves the first level entirely to you. That is less than a framework gives you and more than it sounds: a FlagSet is a complete, independent parser with its own name, its own registered flags, its own usage output and its own leftover operands. ```go fs := flag.NewFlagSet("add", flag.ContinueOnError) force := fs.Bool("force", false, "overwrite existing entries") err := fs.Parse(argv) ``` Everything the package-level functions do (`flag.Bool`, `flag.Parse`, `flag.Args`) is a method on a FlagSet; the package-level ones simply operate on one particular set, `flag.CommandLine`, which is created with `flag.ExitOnError`. ## Splitting the argument vector The dispatch is ordinary Go: 1. `os.Args[1:]` is everything after the program name. 2. Element 0 of that slice is the subcommand name — unless it starts with `-`, in which case the user passed a global flag or asked for help. 3. Everything after the subcommand name belongs to that subcommand's set: `fs.Parse(os.Args[2:])`. Getting the slice bounds right is the whole trick. Passing `os.Args[1:]` to the subcommand's set makes the set try to parse the verb itself, which is a non-flag token, so parsing stops immediately and every flag keeps its default — a silent failure rather than an error. ## Choosing the error-handling mode `flag.NewFlagSet` takes an `ErrorHandling` value, and the choice has real consequences: - **`flag.ContinueOnError`** — Parse returns the error to the caller. This is what you want when `main` owns error reporting, and it is the only mode that lets you unit-test parsing, because the test process survives a bad argument. - **`flag.ExitOnError`** — Parse prints the failure and the usage text and terminates the process itself. Convenient for a small tool, awkward for anything that wants a single place where the program ends. - **`flag.PanicOnError`** — Parse panics. Occasionally useful in tests. With `ContinueOnError`, asking for `-h` or `-help` when no such flag is defined makes the set print its defaults and return the sentinel `flag.ErrHelp`. Comparing against that sentinel is how you distinguish "the user asked for help" from "the user typed something wrong", which usually leads to different output. ## Per-command help Each FlagSet has a `Usage func()` field. Setting it gives that verb its own synopsis: ```go fs.Usage = func() { fmt.Fprintf(fs.Output(), "usage: tool add [-force] <file>...\n") fs.PrintDefaults() } ``` `fs.PrintDefaults()` renders the generated one-line-per-flag block from the names, defaults and usage strings you registered, and `fs.SetOutput` / `fs.Output` control where it goes — useful when a test wants to capture the text instead of letting it reach standard error. ## Where global flags live This is the design decision the pattern forces on you, and there is no default answer. Two workable contracts: - **Global flags before the verb.** Register them on `flag.CommandLine`, call `flag.Parse()` first, and take `flag.Args()` as the subcommand and its arguments. Users must then write `tool -v add file`, because the verb is an operand that stops the top-level parse. - **Global flags on every set.** Write a small helper that registers the shared flags onto each subcommand's FlagSet. Users then write `tool add -v file`, and nothing works before the verb. What you must not do is register a global flag on one set and read it on a path that parses a different set: the flag is defined, never parsed, and holds its default with no complaint. Whichever contract you pick, state it in the top-level usage text, because users will script against it. ## The shape that scales A structure that survives growth is a small `command` value holding a name, a one-line description and a `run(argv []string) error`, with the FlagSet constructed inside `run`. The dispatcher is then a lookup plus a call, `main` prints the list of commands when the lookup fails, and every command's flags stay next to the code that uses them.
- What does the ErrorHandling argument to flag.NewFlagSet change?`flag.ContinueOnError` makes Parse return the error to you, which keeps error reporting in one place and makes parsing unit-testable. `flag.ExitOnError` makes the set print usage and end the process itself. `flag.PanicOnError` panics instead. With ContinueOnError, `-h` with no such flag defined prints the defaults and returns the sentinel `flag.ErrHelp`, so you can treat help differently from a bad argument.
- Where do global flags like -v live when every subcommand has its own set?Either register them on `flag.CommandLine` and parse it first — which forces users to write `tool -v add file`, since the verb is an operand that stops the top-level parse — or register them on every subcommand's set with a shared helper, so `tool add -v file` works. Pick one and put it in the usage text; users will script against whichever you chose.
- How do you produce help text for one subcommand?Set that set's `Usage` field to a function printing the command's synopsis and then calling `fs.PrintDefaults()`, which renders the registered flags with their defaults and usage strings. `fs.SetOutput` redirects that text, which lets a test capture the output instead of letting it reach standard error, and `fs.Output()` returns the writer currently in use.
saying these in an interview costs you the question
- Registers every subcommand's flags on flag.CommandLine
- Passes os.Args[1:] to a subcommand's Parse, verb included
- Assumes the flag package dispatches subcommands itself
- Uses ExitOnError everywhere, then cannot test parsing
- Treats an unknown verb as an operand instead of an error