# The argv grammar

A usage spec says what a CLI accepts. This page says how a command line is
matched against it: which token binds to which flag or argument, when a word
selects a subcommand, and what counts as an error.

It exists because that behavior was previously defined only by
[usage-lib](https://github.com/jdx/usage/tree/main/lib)'s implementation. A
second implementation — in Rust, in Go, in a shell completion — had no way to
know whether it agreed, and no way to prove it. The grammar here is normative,
and the [conformance corpus](#the-conformance-corpus) makes it executable.

::: tip Both implementations answer every vector

usage-lib and usage-argv agree with every vector today. That is a
measurement, checked on every run rather than asserted here — see
[Where the reference implementation differs](#where-the-reference-implementation-differs).

:::

## Seeing it for yourself

`usage explain` answers this page for one command line: which token bound to
which flag or argument, and where every value that no token supplied came from.
The example below is [`examples/explain.usage.kdl`](https://github.com/jdx/usage/blob/main/examples/explain.usage.kdl),
and its output is checked against the real thing by a snapshot test.

```sh
usage explain -f examples/explain.usage.kdl \
    -e MYCLI_COLOR=never -e MYCLI_PROFILE=prod \
    -- mycli -j8 --env=prod build a -- --raw
```

```text
mycli -j8 --env=prod build a -- --raw
command  mycli build

tokens
  [0]  mycli       program
  [1]  -j8         flag -j, value of jobs = "8", attached
  [2]  --env=prod  flag --env, value of env = "prod", attached
  [3]  build       subcommand build
  [4]  a           arg target = "a"
  [5]  --          separator
  [6]  --raw       arg extra = "--raw"

values
  flag  --jobs       8      argv [1]
  flag  --env        prod   argv [2]
  flag  --color      never  env MYCLI_COLOR
  flag  --profile    prod   env MYCLI_PROFILE
  flag  --strict     true   default_if --profile when="prod"
  arg   <target>     a      argv [4]
  arg   [-- extra]…  --raw  argv [6]

shadowed
  flag  --jobs   default 1     lost to argv [1]
  flag  --color  default auto  lost to env MYCLI_COLOR
```

Three tables because no one of them is enough. `tokens` cannot show a value that
came from nowhere in argv; `values` cannot show a token that bound to nothing;
and `shadowed` answers the question neither does — which declaration would have
supplied a value, and what beat it.

The reference implementation is the one answering, so the report is what the
grammar below says rather than what any particular reading of it assumes. It
exits 0 even when the explained command line does not parse — that being the
case a report is most wanted for. Pass `--format json` for the same facts as
data.

## Terms

A **token** is one element of `argv`, after the shell has finished with it. The
grammar never re-splits a token on whitespace: quoting is the shell's job and is
already done.

A **command line** is the tokens after the program name. `mycli install -f x`
has three.

When the spec declares `multicall`, argv[0]'s basename is itself a word: a
symlink `ls -> busybox` is parsed as if the first token were `ls`. The
dispatcher names (`name` and `bin`) are skipped, so `busybox ls` still has two
words after the program name. Path components and a trailing `.exe` are
stripped so `/usr/bin/ls` and `ls.exe` select the same applet.

A token is **flag-like** when it begins with `-` and is longer than one character.
Negative numbers normally remain values, but an exact two-byte spelling such as
`-0` is a flag when that digit short is explicitly declared. This lets fd keep
`-0` / `--print0` without turning undeclared `-1`, `-2.5`, or `-1e5` into flags.

A number here means digits, at most one `.`, and optionally an exponent — `e` or
`E`, an optional `+` or `-`, then at least one digit. So `-1`, `-2.5`, `-1e5`, and
`-1.5e-3` are values. It is deliberately narrower than what a float parser
accepts: `-inf` and `-NaN` parse as floats but are far likelier to be misspelled
flags than numbers somebody meant to pass. `-1x` and `-1e` are not numbers either,
and so name flags that do not exist.

## Reading a command line

Tokens are read once, left to right. There is no backtracking, no reordering, and
no second pass: what a token binds to is decided when it is read, from the
command in scope at that moment. This is what makes the grammar implementable as
a single loop, and it is also why a `--` or a subcommand word changes the meaning
of everything after it but nothing before it.

At each token, in order:

1. If flag interpretation has stopped (a `--` was consumed), the token is a
   value.
2. If the token is exactly `--`, flag interpretation stops. The token is consumed
   and is not itself a value.
3. If the token is flag-like, it is matched as a flag ([long](#long-flags) or
   [short](#short-flags)). If nothing matches, see
   [unrecognized flags](#unrecognized-flags).
4. Otherwise the token is a word: it selects a [subcommand](#subcommands) if one
   matches; otherwise it is forwarded as an [external subcommand](#external-subcommands)
   if the command declares `external_subcommand`; otherwise it is offered to the
   command's [positional arguments](#positional-arguments).

## Long flags

A token beginning with `--` is a long flag. The name is the text up to the first
`=`, or the whole token if there is none.

**Names match exactly.** `--for` does not match `--force`. Abbreviation
inference is deliberately absent: it makes adding a flag a breaking change for
anyone who typed a prefix that was unique until the new flag arrived. A prefix
that matches nothing is then an [unrecognized flag](#unrecognized-flags).

For a flag that takes a value, the value comes from one of two forms:

| form       | value                                                 |
| ---------- | ----------------------------------------------------- |
| `--jobs=8` | the text after the first `=`, so `--set=a=b` is `a=b` |
| `--jobs 8` | the following token                                   |

`--jobs=` binds the empty string. Present-but-empty is a distinct state from
absent, and the attached form is the only way to express it in one token.

A value attached to a flag that takes none is dropped: `--force=yes` sets `force`
and nothing else. Handing the leftover to the positionals would re-split one
token into two and fill an argument the caller never typed a word for.

A detached value **must not be flag-like**, unless the flag declares
`allow_hyphen_values`. `--jobs --force` is a missing value, not a `jobs` of
`"--force"`, because the overwhelmingly likely reading is that the value was
forgotten. To pass a value that begins with a dash, attach it: `--jobs=--force`.
The negative-number exception means `--offset -1` still works, and a lone `-` is
a value too, so `--file -` reaches the flag. A `--` is flag-like, so it stays the
separator rather than becoming the value of whatever flag precedes it.

A flag declared `allow_hyphen_values` takes the following token whatever it looks
like, including `--` and tokens that name other flags. The attached form is then
no longer the only way to pass a dash-prefixed value. A variadic occurrence still
stops collecting at a later flag-like token, so a second occurrence of the same
flag is not eaten as a value.

A flag declared `require_equals` accepts only the attached form: `--inspect=9229`
binds and `--inspect 9229` is a missing value. A short's attached form
(`-i9229`, `-i=9229`) still binds.

A boolean switch declared `bool_value` additionally accepts the exact attached
long forms `--flag=true` and `--flag=false`. It never takes a detached value:
`--flag false` binds the flag and leaves `false` to the positionals. A negated
spelling applies the attached value to that spelling, so `--no-flag=false`
binds the flag true.

A flag declared `default_missing` binds that string when it is given with no
value: `--color` is `always` if the spec says `default_missing="always"`. An
attached or (unless `require_equals`) detached value still wins. A following
flag-like token is not taken as the value, so `--color --verbose` colours with
the missing default and still sets verbose. Combined with `require_equals`, a
following word is a positional rather than the value.

A flag needing a value that ends the command line is an error.

A flag may declare **several long names**; the extra ones are aliases, and every
one of them binds under the flag's name. A flag given **more than once** keeps
the last value, unless it is `var` or `count`, which is what those declare
instead. A repeat is a correction — usually a wrapper appending to a command line
it did not write — so the last word on the subject is the one that counts.

### Flags that take several values

A flag whose argument is variadic (`--include <pattern>...`) keeps taking values
from one occurrence: it consumes following tokens until one is flag-like, or a
`--` arrives, or its `var_max` is reached, or the command line ends. So
`--include a b` gives it both, while `--include a --force` gives it only `a`. The
attached form settles where the _first_ value came from and nothing more, so
`--include=a b` collects both as well.

This is greedy, and a command that declares both a variadic flag and positionals
will find the flag eating them. That is inherent to the feature rather than a
quirk of this grammar; the ways to end the run are `--` and a `var_max`, which
stops the flag and leaves the following words to whatever comes next.

A flag declared `var` without a variadic argument is the other shape: it takes
one value per occurrence and may be repeated, so `--include a --include b`
collects two. The distinction matters — `--include a b` gives a _repeatable_ flag
only `a`, leaving `b` to a positional, while a _variadic_ one takes both.

## Short flags

A token beginning with a single `-` is one or more short flags. Letters are read
left to right.

A letter whose flag takes no value simply sets it, and reading continues with the
next letter — so `-ab` sets both `a` and `b`.

A letter whose flag takes a value ends the token. Its value is:

| form   | value                                       |
| ------ | ------------------------------------------- |
| `-j8`  | the rest of the token                       |
| `-j=8` | the rest of the token after one leading `=` |
| `-j 8` | the following token                         |

So `-aj8` sets `a` and gives `jobs` the value `8`. The attached form is what
makes `-C/tmp` and `-Edev` work.

One `=` immediately after the letter is a separator, matching the long form. Only
one: `-j==8` is a value of `=8`.

A token containing an unrecognized letter is not a bundle at all, so none of its
letters are applied: `-az` does not set `-a` on the way to discovering that `z`
names nothing. What happens to the token instead is described under
[unrecognized flags](#unrecognized-flags).

`-h` is recognized, and so is `-V` on the root of a CLI that declares a version.
A parser supplies both rather than a spec declaring them, and they are letters
like any other: `-vh` is a bundle, and it asks for help. A spec that declares the
letter itself keeps it — `-h` is supplied only where nothing else claims it, so a
CLI whose `-h` means `--host` reads `-vhlocal` as its own.

`-` alone is not a flag. It is a value, conventionally meaning stdin.

## Unrecognized flags

A flag-like token that names no flag in scope **becomes a word**, and is offered to
the positional arguments like any other. With nothing left to hold it, that is an
`unexpected_arg` — the same error an extra word produces, rather than a special one
about flags.

This is where the grammar parts company with every comparable parser. clap,
argparse, commander, oclif v2+, and POSIX `getopt` all reject the token. They are
right for what they do, which is parse _their own_ argv, where a dash-word can only
be a flag or a typo. A usage spec is also used to parse command lines whose flags it
does not own:

- a shell script run through `usage exec`, forwarding options to a tool it wraps
- a task's arguments, where the task script is the authority on what it accepts
- a completion, asked about a line that is still half-typed

In all three, a token the spec has not heard of is far more likely to be data in
transit than a mistake, and refusing it would break the wrapper for everyone who
did not enumerate the flags of the program behind it.

The cost is real and worth stating plainly: a misspelled `--hekp` becomes an
argument instead of an error, and whether it does depends on whether a positional
is free to take it. A CLI that owns all of its flags can have the stricter reading
by asking:

```kdl
unknown_flags "error"     // for the whole CLI
cmd "exec" unknown_flags="value"   // except here, which forwards a command line
```

Unlike `effect`, this **is** inherited: the nearest enclosing command that states a
preference wins, then the spec, then `value`. It describes how a command line is
read rather than what a command does, and a CLI that forwards options tends to
forward them at every level.

Even when refusing, a lone `-` and a negative number stay values — neither is a
misspelled flag, and without the second `--offset -1` could not be written. oclif
made exactly this mistake when it switched to refusing unknown flags, and had to
add the number case back afterward.

## Positional arguments

A word that does not select a subcommand is offered to the command's arguments in
declaration order. Each argument takes one word, except a variadic (`var=#true`,
or a trailing `...`), which collects every word still available.

- A word offered when no argument can hold it is an error, not silently dropped.
- `var_max` is a limit rather than a check: a variadic stops once it is full and the next
  argument takes the rest, which is what makes `[a]… [b]` fillable at all. An argument
  after an _unbounded_ variadic still can never be filled. clap's `num_args` behaves this
  way, and specs are commonly generated from clap commands.
- `var_min` is a check, since nothing about a word tells you a variadic will end up short.
  It is enforced once the last token has been read.
- An unfilled required argument is an error. An unfilled optional one is absent.

Flags and words may interleave freely: `ex from -f to` fills the same arguments
as `ex -f from to`. A flag between two words does not affect which argument each
word fills.

## Subcommands

A word is matched against the subcommands of the command in scope, by name and by
alias. A match descends: parsing continues against the subcommand, and the
selected path records the canonical name even when an alias was typed.

**A name outranks an alias.** The word is matched against every subcommand's
name first; only if none answers is it matched against their aliases. So
declaration order never decides which command a word selects — reordering `cmd`
blocks cannot change what a command line means — and no command's own name can
be shadowed by another command's alias.

That precedence only ever comes up in a spec that should not exist: one command's
alias equal to another's name leaves one of the two unreachable whichever way it
is resolved. `usage lint` reports it as `duplicate-subcommand`, and a derive
rejects it at compile time. The rule is stated because a parser handed a spec it
did not validate still has to answer, and every implementation should answer the
same way.

**A subcommand name wins over a positional value.** A CLI that declares both
cannot receive a positional whose text equals one of its subcommand names — mise
documents exactly this hazard for tasks that share a name with a command.

**Only the descent position routes.** Once a word has been consumed by a
positional argument, a later word matching a subcommand name is just a value.
`ex other install` does not run `install`.

### External subcommands

A command may declare `external_subcommand`. An unmatched word that names no
subcommand is then the name of an external command, and every token after it is
forwarded with it — including flags. Known subcommands still win. A
`default_subcommand` still catches first. A flag-like token on the parent is
still an unknown flag, not a forwarded name: `ex git --help` forwards
`git --help`; `ex --wat` errors.

This is clap's `allow_external_subcommands`. It is not `unknown_flags=value`,
which is the reading a wrapper uses when hyphen-taking tokens should bind as
values of _this_ command.

```kdl
unknown_flags "error"
external_subcommand #true
cmd "install"
```

The property is per command, so a nested `cmd` can forward while the root still
owns its flags:

```kdl
cmd "exec" external_subcommand=#true {
  cmd "install"
}
```

### Multicall

A spec may declare `multicall`. argv[0]'s basename is then a word: a symlink
`ls -> busybox` is parsed as if the first token were `ls`. The dispatcher names
(`name` and `bin`) are skipped, so `busybox ls` still selects `ls`. Path
components and a trailing `.exe` are stripped, so `/usr/bin/ls` and `ls.exe`
select the same applet.

An unknown applet is an unmatched word: it errors, or is forwarded if the root
declares `external_subcommand`. Invoking the dispatcher with no further words
stays at the root.

This is clap's `Command::multicall`. Corpus vectors that care about argv[0]
carry it as `argv0`; everyone else is the spec's `bin`.

### Flag scope

A flag belongs to the command that declares it and may appear anywhere that
command is in scope, which includes before one of its own subcommand words:
`ex --quiet install` works for a root `--quiet`.

A flag declared `global=#true` is additionally inherited by every command
beneath it, so it may appear after any subcommand word at any depth.

Scope only ever runs downward. A flag declared on a subcommand is not accepted
before that subcommand is reached, `global` or not. A subcommand may redeclare a
name it would otherwise inherit, and below that point its own declaration is the
one that binds — mise does this deliberately, redeclaring several root globals on
`run` with different shorts.

## The `--` separator

A bare `--` stops flag interpretation. Every token after it is a value, however
flag-like it looks. The separator is consumed and is not itself a value.

Only the first `--` is a separator. A later one is an ordinary value, since flag
interpretation has already stopped — which is what lets a CLI forward a command
line that itself contains `--`.

An argument may say more about its relationship to the separator, with
`double_dash`:

| mode        | meaning                                                             |
| ----------- | ------------------------------------------------------------------- |
| `optional`  | the default: values may appear on either side                       |
| `required`  | values are accepted only after a `--`; a word before it is an error |
| `preserve`  | the separator is kept as a value instead of being consumed          |
| `automatic` | once the argument takes a value, behave as if `--` had been given   |

## Values not from argv

When the command line does not supply a value, it is taken from the environment
if the flag or argument declares `env`, and otherwise from its `default`. In
short: **command line, then environment, then default.**

An environment variable set to the empty string is set. Treating empty as unset
would make `EX_JOBS=` mean something no other empty value in the grammar means.

A variable's value is one value, however much whitespace or however many commas
it contains. It is not a command line, and is never split into several.

For a flag that holds no value there is nothing to bind, so the variable's text
is read as a boolean: `1`, `true`, `True`, and `TRUE` set it, and anything else —
`0` and `false` included — leaves it false. Presence cannot be the test, since a
parent process that exports `EX_VERBOSE=false` is saying no. Note that this is
narrower than clap's falsey parser, where any value that is not falsey is true,
so `yes` sets the flag there and not here.

None of this changes which token binds where. It only fills what the command line
left empty, which is why it is described last: an implementation can do all of it
after the single pass is over.

## Warnings

A declaration may still work and still be something a CLI wants to stop people using.
`deprecated` on a flag or a command says so, and `deprecated_env` says it about one of
the variables a value may arrive through. Help and completions describe those already;
this is what a parse says to the person who just used one.

A parse **reports rather than prints**. A parser that wrote to stderr could not be used
by anything with an opinion about output, and mise queues its deprecations until its
logging is up — the same rule [configuration resolution](/spec/resolution#warnings)
follows. What prints is the CLI's own process entry point, not the parse.

Each warning has a **kind**, because the wording is for a person and the kind is what a
program acts on:

| kind                 | what happened                                                  |
| -------------------- | -------------------------------------------------------------- |
| `deprecated-flag`    | a flag whose declaration says not to use it any more was given |
| `deprecated-command` | a selected command's declaration says not to use it any more   |
| `deprecated-env`     | a value arrived through a `deprecated_env` alias               |

What a parse reports is a _set_, not a sequence. Every deprecation a command line used
is reported once, and the arrangement is a quality-of-implementation matter: two
implementations agree on which warnings there are without having to agree on the order
they come out in. A caller that cares about arrangement sorts by `kind`.

Three rules decide whether there is anything to say.

**The command line and the environment count; a default does not.** A flag supplied by a
variable used the deprecated declaration as much as a typed word did, and both are things
the user arranged. A declared default is nobody's request, so a deprecated flag with a
default does not warn on a command line that never mentioned it.

**Every deprecated command on the selected path reports**, not only the last one: a
deprecated group whose child is fine was still the way in. The root is not one of them —
it is the program that is running, not something the command line selected. Neither is a
command a [`view`](/rust/subcommands#executable-views) promoted, nor any command the view routes through:
under `aubr` those words _are_ the program's identity, so an implementation that reaches
them by rewriting `argv` reports no more than one that reads a spec whose root is already
the promoted command.

**`deprecated_warn_at` is an author saying _not yet_.** A warning is withheld until the
CLI's own `version` reaches that release. `deprecated_remove_at` never withholds anything
and is never an error: removing a declaration is the author's job, not the parser's, and
until then the milestone is something the message mentions.

Versions compare as dotted integers, left to right:

| case                                  | result                                               |
| ------------------------------------- | ---------------------------------------------------- |
| no `deprecated_warn_at`               | warn — it is deprecated now                          |
| the CLI declares no `version`         | warn                                                 |
| either version is not readable as one | warn                                                 |
| unequal segment counts                | missing segments are `0`, so `2026.12` = `2026.12.0` |
| a `-suffix`                           | sorts before the same numbers without one            |
| a `+suffix`                           | ignored                                              |

Semver and calver both work under that rule. Every uncertain case warns on purpose: noise
is recoverable and silence is not, so a version string an implementation cannot read
produces a warning rather than hiding one.

## Errors

The grammar distinguishes these classes of failure. Wording is not specified —
diagnostics are a quality-of-implementation concern and should be much better
than these names — but the class is, so a strict parser and a lenient one can be
told apart mechanically.

| code                       | when                                                      |
| -------------------------- | --------------------------------------------------------- |
| `unknown_flag`             | a flag-like token matched no flag in scope                |
| `missing_flag_value`       | a flag needing a value did not get one                    |
| `missing_required_flag`    | a required flag never appeared                            |
| `missing_required_arg`     | a required argument was never filled                      |
| `unexpected_arg`           | more words than the command can hold                      |
| `invalid_choice`           | a value outside the declared `choices`                    |
| `arg_requires_double_dash` | a `double_dash="required"` argument got a value too early |
| `var_too_few`              | fewer values than `var_min`                               |
| `var_too_many`             | more occurrences than a repeatable flag's `var_max`       |
| `conflicting_flags`        | two flags declared to `conflict` were both given          |

Choices match exactly; case-insensitive matching would have to be declared rather
than assumed.

## The conformance corpus

[`corpus/`](https://github.com/jdx/usage/tree/main/corpus) holds the executable
form of this page: JSON vectors pairing a spec and a command line with the
expected result.

```json
{
  "id": "long-value-attached",
  "doc": "`--flag=value` binds the text after the first `=`.",
  "spec": "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\n",
  "argv": ["--jobs=8"],
  "expect": { "ok": { "flags": { "jobs": "8" } } }
}
```

Bindings are keyed by the name the spec gives each flag and argument, never by
the token that set them, so `-j`, `--jobs`, and `EX_JOBS` all land under `jobs`.
Values are recorded as strings: the grammar decides which token binds where, not
what it means, so turning `"8"` into a number is the caller's business. Failures
record only the error code.

Vectors that set `env` carry their own environment. The harness never reads the
process environment, so no vector's result can depend on the machine running it.

Any implementation in any language can run these. In this repository,
`cargo test -p usage-conformance` runs them against both usage-lib and
[usage-argv](https://github.com/jdx/usage/tree/main/argv), and `mise run test:go`
runs them against [usage-go](https://github.com/jdx/usage/tree/main/go), which is a
second implementation of this page in another language — the thing the corpus was
made to make possible.

Each vector says which layer of a parser it is a question for. Most are
`binding` — which token becomes which flag or argument — and a parser that reads
argv is expected to answer all of those. The rest are `post-binding`: `required`,
`choices`, `env` fallback, defaults, `var_min`, `overrides`, and `conflicts` are
decided once the last token has been read, and need to know a value's type, so a
binding-only parser leaves them to the layer that owns the target struct.

That is declared per vector rather than worked out from the spec, because an
implementation should be told which vectors apply to it. usage-lib answers every
vector; usage-argv answers the binding ones.

## Where the reference implementation differs

Each vector records whether usage-lib agrees with it, as a measurement rather
than an assumption, and
[`conformance/tests/reference.rs`](https://github.com/jdx/usage/blob/main/conformance/tests/reference.rs)
fails if a label is wrong in either direction. A recorded divergence that gets
fixed shows up as a test failure telling you to delete the label, so the list
cannot rot.

**Today it does not: usage-lib agrees with every vector.** The list is empty
for the first time, and the five entries it used to hold were what writing the
grammar down was for. Each was a real defect that only a second reading found:

- `--jobs=--force` bound `force` and left `jobs` unset, because an attached value
  went back on the token queue and was read a second time as a flag of its own.
- `ex --jobs -- x` gave `jobs` the word after the separator, so the command line
  quietly meant `ex --jobs=x` and the `--` was gone.
- `--include a b` gave a variadic flag argument only `a` and called `b`
  unexpected, though the [flag reference](/spec/reference/flag) documents
  `--include <pattern>...` as the form that collects.
- The same gap made a `var_max` on such a flag unreachable.
- `double_dash="automatic"` was declared and serialized but never enforced.

An empty list is a state, not a promise. The next rule written here will very
likely land before the parser does, and the label is how that gets said out loud
rather than discovered by whoever writes the next implementation.

## Not yet covered

- **Restart tokens.** `restart_token` (mise's `:::`) makes one command line
  describe several invocations, which the vector format cannot express: `expect`
  holds a single result. Supporting it needs a multi-invocation shape, and until
  then usage-lib's behavior — rewind, and let the last invocation's bindings
  stand — is untested here.
- **Completion parsing.** `parse_partial` deliberately accepts incomplete input
  to drive completions. It is a different contract with different expectations,
  and deserves its own corpus.
