You own a Go CLI other teams call from CI. How do you decide what becomes a subcommand, a flag, or a positional?
answer
- the command line is a published interface
- additions are safe, edits are not
- a changed default breaks nothing loudly
- the people scripting it pay for renames
basics
~20 sTreat the command line as a public API. Subcommands for distinct verbs, flags for anything optional or likely to change, positionals only for the one obvious operand — Go's flag package offers no alias or deprecation mechanism once a name ships.
solid answer
~50 sOnce a name is in a thousand shell scripts it is permanent, so I design for what can be added rather than what can be changed. Distinct verbs become subcommands, because a `*flag.FlagSet` per verb keeps each command's options and help scoped and lets new commands appear without touching old ones. Anything optional becomes a flag: adding one with a safe default is backward compatible, while renaming a flag or changing an existing default is breaking with no compile-time signal — scripts either fail at runtime or silently do something different. Positionals get the single obvious operand and nothing more. I also fix the mechanical constraints on day one: flags must precede operands, booleans need `-q=false`, and if arguments are ever forwarded onward, `--` belongs in the published synopsis from the first release. Then I take the surface to the teams who script the tool, because they pay for a rename, not me.
code
go · 4 linesvar out string
fs.StringVar(&out, "output", "", "write results here")
fs.StringVar(&out, "o", "", "short form of -output")
// Both write into out; whichever appears later on the command line wins.go deeper
Know that a tool's flag names end up in other people's scripts, so they are chosen once. Prefer adding a new flag over changing what an old one does.
Be able to place a value: subcommand for a distinct verb, flag for anything optional, positional for the one obvious operand — and explain the flags-before-operands rule that constrains all three.
Rank the changes by blast radius and show you know which failures are loud and which are silent, especially that a changed default rewrites the behaviour of every invocation already in a pipeline.
Own the surface as a contract with the teams who script it: circulate names before the first release, evolve only by addition, keep an old spelling bound to the same variable while it is retired, and ship a version subcommand so callers can assert what they invoke.
## The command line is an API without a compiler A Go CLI's argument surface has every property of a published interface and none of the protections. When you rename a method, every caller fails to build. When you rename a flag, a shell script fails at 3am with a message about an unknown flag — or worse, keeps running and does something else. There is no deprecation period baked into `flag`, no alias mechanism, no warning channel. The design question is therefore not "what reads best today" but "what can I add later without breaking anyone". ## What is safe to change, and what is not Ranked from harmless to dangerous: 1. **Adding a new subcommand.** Safe. Nothing that exists changes meaning. 2. **Adding a new flag with a default that preserves current behaviour.** Safe, and it is the mechanism you should design toward. 3. **Adding output or extending help text.** Usually safe, unless somebody parses your output — which is why machine-readable output belongs behind an explicit flag. 4. **Renaming a flag or a subcommand.** Breaking, but loudly: unknown flags are reported. 5. **Changing an existing flag's default.** The most dangerous change available to you, because every invocation already in existence keeps working and quietly does something new. If a default must change, do it by adding a new flag or a new subcommand, not by editing the old one. 6. **Changing the meaning or order of positional arguments.** Also silent, and worse, because there is no name to key on. ## Choosing the form **Subcommand** when the action is genuinely a different verb with a different set of options. A `*flag.FlagSet` per verb is the concrete payoff: each command's flags and help are scoped to it, two commands can use the same short name for different things, and adding a seventh command touches nothing the other six do. The alternative — one mode flag plus a pile of options that are only meaningful in some modes — has no way to express that scoping, and its help text degrades into a list of caveats. **Flag** for anything optional, anything with a sensible default, and anything you can imagine wanting to vary. Flags are named, so they are self-documenting at the call site and order-independent among themselves. The cost of one extra flag is nearly zero; the cost of having made something positional that should have been a flag is permanent. **Positional** for the single obvious operand — the file, the path, the identifier the command is *about* — and for variadic lists of the same kind of thing. Two positionals of different meaning is a smell: `tool copy src dst` is defensible because the order is a universal convention, `tool report 30 prod` is not. ## Mechanical constraints to publish on day one Go's parser imposes rules that become part of your contract whether or not you write them down: - **Flags must come before operands.** Parsing stops at the first non-flag token, so `tool sync out.txt -v` silently ignores `-v`. Publish the synopsis as `tool [flags] <operands>`. - **Boolean flags need the equals form** to be turned off: `-q=false`, never `-q false`. - **One dash and two dashes are identical**, and there is no bundling. If people expect a short alias, register it explicitly. - **`--` ends flag parsing.** If your tool will ever forward arguments to something else, define that boundary in the first release; adding it later changes the meaning of commands already written. ## The alias you do have When a name really must change, `flag` gives you exactly one affordance: register both names bound to the same variable using the `Var` forms, with the same default. ```go var out string fs.StringVar(&out, "output", "", "write results here") fs.StringVar(&out, "o", "", "short form of -output") ``` Both write into `out`, so whichever appears later on the command line wins. Give them the same default or the second registration will overwrite the first at definition time. This is how you keep an old spelling alive while the new one becomes the documented form — and you keep it alive for a long time, because you cannot see who is still using it. ## Who actually decides The maintainer proposes the surface; the teams whose pipelines invoke the tool can overrule it, and they should be able to, because they carry the migration cost of every rename. Practically that means: circulate flag names before the first release rather than after, treat any change in category 4-6 above as requiring their agreement, and keep a `version` subcommand from day one so a script can assert what it is talking to. The thing you are optimising is not elegance; it is the number of shell scripts that never have to be edited again.
- Why is changing an existing flag's default more dangerous than renaming it?A rename fails loudly: the parser reports an unknown flag and the pipeline stops. A changed default keeps every existing invocation valid while quietly giving it new behaviour, so the failure surfaces as wrong output somewhere downstream, possibly weeks later. If behaviour must change, introduce a new flag or a new subcommand and leave the old default alone.
- When would you accept a mode flag instead of splitting into subcommands?When the two modes genuinely share every option and differ only in one axis — an output format, for instance. Once each mode wants options the other cannot use, a subcommand is better, because a `*flag.FlagSet` per verb scopes those options and their help text, whereas a mode flag leaves you documenting which flags are meaningful when.
- How do you evolve a CLI's surface once it is embedded in other teams' pipelines?By addition, and with the consumers in the room. New subcommands and new flags with behaviour-preserving defaults are safe; renames and default changes need agreement from the teams who will edit the scripts. Keep the old name registered against the same variable while the new one becomes documented, ship a version subcommand so scripts can assert what they are calling, and never assume you can see all the callers.
Flag names are like the column names of a table other teams query directly: adding a column is routine, renaming one is an outage you schedule with everybody who reads it.
saying these in an interview costs you the question
- Treats flag names as an implementation detail
- Renames flags for tidiness in a minor release
- Changes an existing default instead of adding a flag
- Makes several unrelated values positional arguments
- Assumes all callers are visible in this repository