ADR 009 Command line shape - nekrozis/qtogo GitHub Wiki
Status: Accepted (2026-10-09).
- The front end was written before this record: the command tree is data in
internal/cli/command.go, the grammar lives inparse.goandoptions.go, the dispatcher inrun.go, and rendering inrender.go. The shape was chosen as it was built, and the plan has carried "a record is still owed for that decision" since. It is owed now becauselist-qtis the first business command and every later one โplan,install,cacheโ will sit on this parser. - The reference tool builds its command line with
argparse, re-deriving the grammar in each subcommand. The plan's D2 question was whether to take a third-party framework โ Cobra and the like โ or hold to the standard library, and it was answered standard library (plan ยง9 D2): the tree-as-data plus a parser/dispatcher split is small, and it keeps the front end dependency-free. -
Amended 2026-10-09. The first cut of
list-qthad made host and target options. With the second business command arriving, decision 7 fixed the family shape โ identity positional, modifiers as options โ and movedlist-qtback onto the form the plan's ยง11 already wrote.
-
The standard library, and no CLI framework. The grammar is a few hundred lines that the project can read and test directly. A framework would own the command shape, the help text and the error wording, which are three of this project's contracts โ ADR-002's exit codes and JSON envelope, the generated help, and the compatibility rows a divergence needs.
-
The command tree is data, and it is the whole vocabulary.
commandTreeincommand.gonames every business verb; a word that is not in it is an unknown command, never a stub that fails later. Help is generated from the same tree, so it cannot advertise a verb the build does not have. -
An option may take a value, written either way.
--modules qtchartsand--modules=qtchartsare the same; a flag that takes no value refuses=value(usage.unexpected_value); a value-taking option whose value is missing or is itself an option token isusage.missing_argument. A repeated value-taking option keeps every value in order, which is how an ordered list is written on a command line (--basefirst, then its mirrors). -
Business commands run through a seam. A command asks the
Servicesinterface for a result and renders it; the front end never reaches past it intodiscovery,transportorrepository. That is what lets a command be tested with neither a process nor a network, and it is what keeps the order of the repository layers ininternal/servicerather than in a command. -
The output and failure contracts are unchanged from ADR-002. Payloads go to stdout, diagnostics to stderr,
--jsonis the machine shape, and the exit code is classified in one place. A command does not invent a code; it returns the failure it was given. -
A listing shows version numbers; the version DTO's JSON is unchanged. A
list-qtdocument carries versions as numbers ("5.15.2", what a user writes in a request), not as the repository's raw tokens ("5152"). That is why the payload is built fromDotted()rather than by marshallingmodel.Version: a listing is a request-shaped report, not a carrier of the repository's spelling. ADR-001 section 1's rule โ a marshalled version carries the raw string โ stays as it is, for a document that does carry a version as its source representation. -
Identity is positional; modifiers are options. What a command acts on is written positionally, and how it behaves is written as options:
list-qt <host> <target>,plan install-qt <host> <target> <version> [arch]. A leaf declares its positional arguments in the tree โ name, summary, and whether the argument is optional โ and only trailing arguments may be optional. Too few isusage.missing_argument, too many isusage.unexpected_argument, and--still ends option parsing, so a positional value may begin with a dash. Help renders the same declarations, so the usage line and the arity check cannot drift.This is the shape the plan's own ยง11 wrote, and the reference tool's: identity positional, behaviour as flags (
git,docker,aptread the same way). The first cut oflist-qtmade host and target options instead; that was an unrecorded deviation from ยง11, and it is corrected here rather than carried forward, because a second spelling for the same idea would spread to every command that follows. -
An option keeps the reference tool's spelling where it has one. An option that names the install directory is
-O/--outputdir, not--output, so a script written against the reference tool does not have to learn a second name for the same idea. Identity is positional (7); a modifier that already has a name in the tool this project is compatible with takes that name.
- Adding a command is a node in the tree, a case in the dispatcher, a renderer and a service method โ four small edits in one place each, and the shape says where.
- The parser is replaceable by construction: the grammar is confined to
parse.go/options.go, so a future change to how a line is read does not reach the commands. - The front end costs no dependency, and its help text cannot drift from the vocabulary because both come from the tree.
- Two option syntaxes (
--name value,--name=value) and a rule that a value cannot be another option token are conventions this record fixes; a later change to them is a change to this decision, not a detail. - A command's positional arguments are declared in the same node that names its options, so what a command accepts, and in what order, is data the parser, the arity check and the help all read โ the same reason the option set is data.
- A framework (Cobra, urfave/cli). Rejected: it would own the command shape, the help text and the error wording, all of which are contracts here, and it adds a dependency for a grammar small enough to test directly.
-
The
flagpackage. Rejected: it models one command's flags, not a tree of verbs, so subcommands and inherited options would be hand-built around it anyway. - Parsing inside each command. Rejected: the grammar would be scattered, and "is this a usage error or a failure" would be decided in as many places as there are commands.
-
Reaching into the layers directly from a command. Rejected: a command would
then know the repository walk, could not be tested without a network, and would
duplicate the order
internal/servicekeeps in one place.