skip to content

Why does Go's flag package ignore -v in `tool build main.go -v`, and what does `--` do?

level: middleimportance: should knowfreq 50%

answer

  1. the order of tokens matters
  2. flags and operands do not interleave
  3. the first non-flag token ends it
  4. two dashes force the rest to be operands

basics

~20 s

Go's flag package stops parsing at the first argument that is not a flag, so -v after main.go is treated as a positional operand and never assigned. A bare -- also ends parsing, and everything after it becomes an operand.

solid answer

~50 s

Parsing is strictly positional: `flag.Parse` walks the arguments from the left and stops at the first token that is not a flag, which is why flags have to come before operands. In `tool build main.go -v`, parsing halts at `build`, so `flag.Args()` is `[build main.go -v]` and `-v` silently keeps its default — no error is reported, because from the parser's point of view those are just operands. A bare `--` ends parsing explicitly and is itself consumed, so `tool -n 3 -- -weird.txt` sets `-n` and leaves `-weird.txt` as the only operand; that is the standard way to pass values that begin with a dash. Two other syntax rules bite: one dash and two dashes are accepted identically, and a boolean flag never consumes the next token, so it must be written `-q=false`, not `-q false`.

code

text · 3 lines
text
tool -n 3 a.txt b.txt    -> n = 3,       Args() = [a.txt b.txt]
tool a.txt -n 3          -> n = default, Args() = [a.txt -n 3]
tool -n 3 -- -weird.txt  -> n = 3,       Args() = [-weird.txt]

go deeper

for a junior

Remember the shape: flags first, then the files or names the command acts on. If a flag seems to be ignored, check whether it was written after an operand.

for a middle

Explain the actual rule — parsing stops at the first non-flag token — and demonstrate the -- terminator plus the -q=false boolean form.

for a senior

Point out that a misplaced known flag produces no error at all, unlike an unknown one, and describe how you would catch that class of bug in review or in a smoke test of the command line.

for a principal

Treat the ordering rule as part of the tool's published contract: document whether arguments are forwarded after --, because changing that boundary later silently rewrites the meaning of commands already in scripts.

## Parsing is left-to-right and it stops Go's `flag` package deliberately implements a small, strict grammar rather than GNU `getopt`. `Parse` walks the argument slice from the left and processes flags until it meets a token that is not a flag. At that point it **stops** and hands everything remaining, starting with that token, to `flag.Args()`. A token is not a flag when it does not start with `-`, when it is exactly `-` (a lone dash, which by convention means standard input and is treated as an operand), or when it is exactly `--` (the terminator, which is consumed and ends parsing). So: ``` tool -n 3 a.txt b.txt -> n = 3, Args() = [a.txt b.txt] tool a.txt -n 3 -> n = default, Args() = [a.txt -n 3] tool -n 3 -- -weird.txt -> n = 3, Args() = [-weird.txt] ``` The middle line is the whole problem: nothing is wrong, nothing is reported, and the flag quietly serves its default. Compare that with an *unknown* flag placed before the first operand, which is an error and produces a usage message. The rule is easy to state and easy to forget: **flags first, operands after**. ## The terminator `--` exists for two jobs. First, it lets an operand that begins with a dash reach your program: without it, `tool -weird.txt` would be parsed as an unknown flag named `weird.txt`. Second, it is the contract a wrapper program advertises — "my flags, then `--`, then everything you want me to forward". Because `--` is consumed by the parser, it does not appear in `flag.Args()`; the arguments after it are handed over verbatim, dashes and all. If you are writing a tool that forwards arguments to something else, decide on that `--` contract on day one and put it in the usage text. Retrofitting it later changes the meaning of every existing invocation. ## The syntax the package actually accepts - `-flag` and `--flag` mean the same thing. Go's parser treats one and two dashes identically; there is no long-versus-short distinction built into the package. - `-flag=value` always works. - `-flag value` works for every type **except** booleans. - There is no bundling: `-abc` is one flag named `abc`, never three flags `a`, `b` and `c`. ## Why booleans are different A boolean flag is normally written on its own — `-q` means true — so the parser cannot also treat the next token as its value; if it did, `tool -q file.txt` would consume `file.txt` as the value. The package therefore refuses to look ahead for boolean flags. The consequence surprises people: ``` tool -q false x -> q = true, Args() = [false x] ``` `-q` is set to true, then `false` is not a flag, so parsing stops and both remaining tokens become operands. To pass an explicit false you must write `-q=false`. Boolean flags accept `1`, `0`, `t`, `f`, `T`, `F`, `true`, `false`, `TRUE`, `FALSE`, `True` and `False` on the right of the equals sign. ## Living with the rule Most of the time the rule is fine — it is what makes the grammar unambiguous without a specification of which flags take values. Three practical habits keep it from hurting: 1. Document the shape as `tool [flags] <operands>` in your usage text, so the ordering is visible. 2. When a subcommand is involved, remember that the subcommand name is itself an operand for the top-level parser; global flags therefore must precede it, and the subcommand's own flags must follow it and be parsed by that subcommand's own flag set. 3. When arguments must be forwarded to another program, define `--` as the boundary and take `flag.Args()` as the forwarded slice. None of this is enforced by the compiler, so it belongs in review: a command line that puts a flag after an operand looks correct and is not.

  • Why must a boolean flag be written -q=false rather than -q false?
    A boolean flag never consumes the following token — otherwise `tool -q file.txt` would swallow the filename as its value. So `-q` is set to true and `false` becomes the first non-flag argument, which also stops parsing. The equals form is the only way to pass an explicit value: `-q=false`, or any of `0`, `f`, `F`, `FALSE`, `False`.
  • Is -name different from --name in Go's flag package?
    No. One dash and two dashes are accepted identically, and both `-name=x` and `-name x` work for non-boolean flags. There is no GNU-style short/long split and no bundling, so `-abc` is a single flag called `abc`, not three one-letter flags. If you want a short alias you register a second flag name yourself.
  • How do you build a tool that forwards arbitrary arguments to something else?
    Define your own flags, call Parse, and publish `--` as the boundary in the usage text: `tool [flags] -- [forwarded args]`. Because the terminator is consumed, `flag.Args()` is exactly the forwarded slice, including tokens that start with a dash. Deciding this up front matters — adding the terminator later changes the meaning of existing invocations.

saying these in an interview costs you the question

  • Assumes flags may appear anywhere, like GNU getopt
  • Writes -verbose true for a boolean flag
  • Thinks the -- terminator appears in flag.Args()
  • Believes -abc means three bundled short flags
  • Expects an error when a known flag is misplaced