skip to content

How do you implement git-style subcommands in Go with flag.NewFlagSet, each owning its own flags?

level: middleimportance: must knowfreq 55%

answer

  1. one parser per verb, not one global one
  2. the argument vector is split before parsing
  3. the first token chooses the flag set
  4. flag.NewFlagSet, then Parse on os.Args[2:]

basics

~10 s

Give 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 s

Go'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 lines
go
func 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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