cmdspec 0.1
A machine-readable description of a command-line interface.
Status: draft. Version 0.x makes no compatibility promise; 1.0 will, and will not be cut until
three CLIs that docspack does not maintain have shipped a document. · Schema:
schema/v0.1.json (JSON Schema 2020-12), published at
https://docspack.dev/cmdspec/schema/v0.1.json · Licence: this text is
CC BY 4.0; the schema and the examples are MIT.
The key words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119.
1. What a document is for
A cmdspec document says everything a caller needs to run a program correctly without running it first: the words it accepts, where each value can come from, what the program reads and writes, what it prints, what its exit status means, and whether it is safe to run. One document serves a person reading a reference page, a shell generating completions, a tool checking a release for breaking changes, and an agent deciding whether a command will delete something.
It describes; it does not implement. Nothing in a document is executed by a consumer, with one
exception that a consumer opts into (valuesFrom, §6.4).
2. The document
A document is a JSON value. It MAY be written as JSON or as YAML 1.2; a YAML document MUST mean
the same JSON value, which in practice means quoting any prose that contains , inside a flow
mapping ({ … }) — unquoted, the comma silently starts a second key. Writing prose in block style
avoids the trap entirely, and tooling that writes YAML SHOULD write it that way.
The conventional file name is cmdspec.yaml or cmdspec.json; examples use
<program>.cmdspec.yaml.
cmdspec: "0.1"
info: { name: greet, version: 1.0.0 }
arguments:
- { name: who, required: true }
options:
- { name: --shout, aliases: [-s], summary: Print in capitals }
examples:
- run: greet world --shout2.1 Root fields
| Field | Type | Meaning |
|---|---|---|
cmdspec | string | Required. The specification version: "0.1". |
info | Info | Required. |
syntax | Syntax | How the parser reads a command line. Defaults to getopt_long’s behaviour. |
environment | EnvironmentVariable[] | Variables read that no option is bound to. |
files | File[] | Files read or written on the program’s own account. |
components | Components | Definitions referenced by $ref. |
The root is also the program’s root command: every command field except name
and aliases may appear on it. A program without subcommands is a document with no commands.
2.2 Info
| Field | Type | Meaning |
|---|---|---|
name | string | Required. The program’s name. |
version | string | Required. The version of the program this document describes. A document describes one version. |
bin | string | The executable a shell runs, when it differs from name. |
summary | string | One line. |
description | string | CommonMark. |
homepage | URI | |
license | { name, identifier, url } | identifier is an SPDX expression. |
contact | { name, email, url } |
2.3 Text
Every summary is one line of plain text with no trailing full stop, written to be read in a table.
Every description is CommonMark and MAY be several paragraphs. A consumer that shows one SHOULD
show the summary in lists and the description on the item’s own page.
2.4 Extensions
Every object except a $ref MAY carry fields whose names begin with x-. Their meaning is not
defined here; a consumer MUST ignore extensions it does not understand. Any other unknown field
makes a document invalid, so a misspelt field is an error rather than a silent no-op.
3. Syntax
syntax says how the program’s parser reads a command line. Every field has a default, and the
defaults are GNU getopt_long’s behaviour, so a document for a conventional program omits it.
| Field | Type | Default | Meaning |
|---|---|---|---|
bundling | boolean | true | Single-character options combine: -abc is -a -b -c. |
shortValue | attached | separate | both | both | -ofile, -o file, or either. |
longValue | equals | separate | both | both | --out=dir, --out dir, or either. |
terminator | string | null | "--" | The word after which nothing is read as an option; null when there is none. |
interspersed | boolean | true | Options may follow positional arguments. |
abbreviation | boolean | false | A long option may be shortened to any unambiguous prefix. |
precedence | (flag | env | config | default)[] | all four, in that order | Where an option’s value comes from, highest priority first. |
A single-character option is one whose spelling is a - and one character. Only those
bundle, and only those follow shortValue; every other spelling follows longValue.
4. Commands
| Field | Type | Meaning |
|---|---|---|
name | string | Required (except on the root). The word that selects this command. It may begin with - — dotnet’s legacy dotnet new --list is a command, not an option — as long as no option of the same command has that spelling. |
aliases | string[] | Other words that select it. |
summary, description | string | |
arguments | Argument[] | Positional arguments, in the order they are written. |
options | Option[] | |
commands | Command[] | Subcommands. |
subcommandRequired | boolean | The command does nothing on its own; a subcommand must be named. |
plugins | { executable, summary } | An unknown subcommand runs another executable. executable contains {name}: git-{name}. |
constraints | Constraint[] | |
exits | Exits | |
io | IO | |
effects, idempotent, interactive, longRunning | Behaviour. | |
examples | Example[] | |
group | string | The heading this command is listed under in help. |
deprecated, since, stability, hidden | Lifecycle. |
A command is addressed by its path: the program’s name followed by each command name,
separated by single spaces — git remote add.
5. Arguments
| Field | Type | Default | Meaning |
|---|---|---|---|
name | string | Required. How the argument is referred to in a usage line, a constraint and prose. Never typed on a command line. It does not begin with - or a space, so it cannot be mistaken for an option, but it may contain spaces: PROJECT | FILE. | |
summary, description | string | ||
value | Value | a string | |
required | boolean | false | |
variadic | boolean | false | Takes one or more words, or zero or more when not required. |
min, max | integer | Bounds on the words a variadic argument takes. | |
rest | boolean | false | Takes every remaining word verbatim, including words that look like options: another program’s command line (kubectl exec pod -- sh -c …). Implies variadic. |
afterTerminator | boolean | false | May be separated from what precedes it by the terminator, and must be when it could be mistaken for something else (git log -- path). |
env | string[] | Variables that supply the value when it is not given. | |
scopedOptions | Option[] | See §6.3. | |
deprecated, since, hidden |
In a valid document, a required argument MUST NOT follow an optional or variadic one unless it is
afterTerminator, since the terminator then marks where it starts; and nothing follows an argument
with rest.
6. Options
| Field | Type | Default | Meaning |
|---|---|---|---|
name | spelling | Required. The canonical spelling, exactly as typed: --output, -o, -name, +x, /q. | |
aliases | spelling[] | Other spellings of the same option. | |
summary, description | string | ||
value | Value | What the option takes. An option with neither value nor values is a switch. | |
values | Value[] | What it takes when followed by several words, in order: jq’s --arg name value. Never with value. | |
variadic | boolean | false | Its value takes one or more words: --property a=1 b=2. Zero or more when the value is optional. |
min, max | integer | Bounds on the words a variadic value takes. | |
qualifier | { separator, value } | A suffix narrowing what it applies to: ffmpeg’s -c:v. | |
required | boolean | false | |
repeat | error | last | list | count | last | What giving it more than once does. count is for switches: -vvv. |
repeatMin, repeatMax | integer | How many times a list option must or may be given. | |
negation | spelling[] | Spellings that undo it: --no-color turns a switch off, --no-author discards a value given earlier. | |
inherited | boolean | false | Accepted by every command below the one that declares it. A command that declares an option with the same name replaces the inherited one, for itself and the commands below it. |
env | string[] | Variables that supply the value when it is not given, first match wins. | |
config | string | The key in the program’s config files that sets it, dots separating nested keys. | |
secret | boolean | false | The value is a credential: prefer the environment variable, and never echo it. |
effects | Effect[] | The command’s effects instead, whenever the option is given with a value other than its default. | |
longRunning | boolean | false | Given, the command no longer returns on its own: --follow, --watch. |
scopedOptions | Option[] | See §6.3. | |
group | string | The heading it is listed under in help. | |
deprecated, since, stability, hidden |
A spelling is written exactly as typed, prefix included. Spelling options literally, rather than
as a name plus a prefix rule, is what lets one format describe --output, -name (find),
-filter_complex (ffmpeg), +x (set) and /q (Windows). No two different options visible to the
same command — its own, its scoped ones and those it inherits — may share a spelling. One
definition listed in several places, as a scoped option often is, is one option.
6.1 Values
A Value’s constraint keywords are JSON Schema’s, so an argument or option converts to a tool’s input schema without translation.
| Field | Type | Default | Meaning |
|---|---|---|---|
name | string | The placeholder in a usage line: file, n, when. | |
type | string | integer | number | boolean | string | |
format | string | What a string denotes. Known formats: path, file, directory, url, email, date, date-time, duration, size, regex, glob, json, key-value, command. Others are allowed and mean nothing to a consumer that does not know them. | |
enum | scalar[] | The only values accepted. | |
default | scalar | The value when none is given. | |
defaultDescription | string | The default when it is computed: “the number of CPUs”. | |
minimum, maximum, pattern, minLength, maxLength | As in JSON Schema. | ||
optional | boolean | false | The option may be given without its value: --color or --color=always. syntax says which forms can carry one. |
separator | string | One word carries several values: --tags a,b,c. | |
stdio | boolean | false | The word - means standard input or output rather than a file of that name. |
valuesFrom | string | A command whose output, one value per line, lists the values currently valid. See §6.4. |
6.2 Referring to an option
Wherever a field refers to an option — constraints, selectedBy, interactive.disable — it
writes the option’s canonical spelling, optionally followed by = and a value:
--output=json, -i=-. This is a reference, not a command line, and is written the same way
whatever syntax says. Arguments are referred to by name.
6.3 Scoped options
Some programs apply an option to the next operand rather than to the whole run. ffmpeg’s
-c:v libx264 out.mp4 sets the codec for that output only, and -ss 10 -i in.mp4 seeks that input
only. scopedOptions on an argument or on an option lists the options that, written before it,
apply to it alone. A scoped option MAY appear in several lists.
6.4 valuesFrom
valuesFrom names a command that lists valid values: kubectl get namespaces -o name. A consumer
MUST NOT run it without the user’s consent, and a document MUST NOT rely on it being run. It
exists because “the valid values are whatever this prints” is the most useful thing a reference
page, a shell or an agent can be told about a value that no static document can enumerate.
7. Constraints
Each constraint has exactly one of these forms. Every name in one is an option reference or an argument name visible to the command.
| Form | Meaning |
|---|---|
exclusive: [a, b, …] | At most one of these. |
oneOf: [a, b, …] | Exactly one. |
anyOf: [a, b, …] | At least one. |
when: x, requires: [a, …] | Whenever x is given, all of these are too. x MAY carry a value: --kind=incorrect. |
Any constraint MAY carry a summary for the message a parser prints when it is broken.
8. What running a command does
8.1 Exit status
exits maps an exit status — a decimal number from 0 to 255, or * for every non-zero
status not listed — to { meaning, description, retryable }. meaning is one line telling the
caller what happened. retryable: true says running the same command again, unchanged, may
succeed.
A command inherits every entry of its ancestors’ exits and MAY override one. So the root
declares what every command means by 0, 1 and 2, and docspack ask adds 3 and 4.
8.2 Standard streams
io has stdin, stdout and stderr, each a list of { mediaType, summary, selectedBy, schema }.
More than one entry means the stream carries different things depending on selectedBy, an
option reference; the entry without one is the default. schema is a JSON Schema for the
content, inline or as a $ref to a file or URL.
io:
stdout:
- { mediaType: text/plain }
- { mediaType: application/json, selectedBy: --output=json, schema: { $ref: ./pod.schema.json } }8.3 Behaviour
| Field | Type | Meaning |
|---|---|---|
effects | Effect[] | What running the command can change. [] means nothing beyond its output; absent means not stated, and a consumer MUST NOT read absence as safety. |
idempotent | boolean | Running it twice with the same input leaves the same state as running it once. |
interactive | { when, disable } | When it prompts — never, tty (only on a terminal) or always — and the option reference that answers every prompt. |
longRunning | boolean | It does not return on its own: a server, a watcher. |
An Effect is one of:
| Effect | Meaning |
|---|---|
read | Reads files or state beyond its arguments. |
write | Creates or changes local state. |
destructive | Removes something, or changes it in a way that cannot be undone. |
network | Talks to another machine. |
exec | Runs other programs or user-supplied code: hooks, scripts, a container’s command. |
effects describes the command as invoked with no options. An option’s own effects replaces the
command’s whenever that option is given with a value other than its default, which is how
--dry-run and --force are described.
8.4 Examples
examples is a list of { run, summary, output, exit }. run is the command line exactly as
typed. output and exit (default 0) are what it prints and returns. An example with output
is a test: a tool MAY run it and compare.
9. Environment and files
environment lists variables the program reads that no option is bound to — NO_COLOR,
GIT_EDITOR — each { name, summary, description, value, secret }. A variable bound to an option
is declared on the option’s env instead, and not here.
files lists { path, role, format, summary }. path may use ~ for the home directory and
$VAR for a variable. role is config, data, cache, state or log. Config files are listed
in the order they are consulted, and an option’s config key refers to them.
10. Components and references
components holds options, arguments and schemas, each a map from a key to a definition.
Wherever an option or argument may appear, { $ref: "#/components/options/<key>" } (or
…/arguments/<key>) MAY appear instead and means exactly the definition it names. A reference
MUST resolve. A referenced option keeps its inherited value, so a shared definition is shared,
not re-declared.
11. Lifecycle
| Field | Type | Meaning |
|---|---|---|
deprecated | boolean | { since, use, summary } | use is what to type instead. |
since | string | The program version that introduced it. |
stability | experimental | stable | Default stable. |
hidden | boolean | Accepted, but left out of help. A document SHOULD still describe hidden options; omitting them makes a document wrong about what the program accepts. |
12. Validity
A document is valid when it validates against the schema and all of these hold. A validator MUST report every rule that fails, not only the first.
- Every
$refresolves. - No two different options visible to one command share a spelling, counting aliases and negations.
- No two subcommands of one command share a name or an alias, and none shares one with an option that command accepts.
- No two arguments of one command share a name.
- A required argument does not follow an optional or variadic one unless it is
afterTerminator, and nothing follows arestargument. minandmaxappear only on a variadic argument or option,variadiconly on an option with avalue, andrepeatMinandrepeatMaxonly on an option whoserepeatislist.- An option with
repeat: counthas no value. - Every name in a constraint, every
selectedByand everyinteractive.disablerefers to an option or argument visible to that command. - A command with
subcommandRequiredhas subcommands orplugins. - A required option has no negation.
13. Discovery
Two conventions let a tool find a program’s document without being told where it is. A program SHOULD follow at least one.
- In a package. An npm package’s
package.jsonMAY carry"cmdspec": "<path>", relative to the package root, naming the document for itsbin. A package with several executables uses an object:"cmdspec": { "<bin>": "<path>" }. The document’sinfo.versionMUST equal the package’s version. - From the program. A program MAY print its document as JSON when run with
--cmdspec, and exit0. It MUST NOT do anything else when given that option.
14. Relationship to other formats
Anything OpenCLI 0.1 or Usage can say converts into a cmdspec document without loss, and converts back to the same document.
- OpenCLI. What both formats can say is mapped field to field. An OpenCLI field cmdspec has no
place for is kept on the same object as
x-opencli-<field>. Going the other way, what OpenCLI cannot say is written into the OpenCLI object’s ownmetadata, under the namecmdspec, so the result is valid OpenCLI and comes back unchanged. - Usage. Usage mirrors clap and has some eighty node properties. Those with a cmdspec field are
mapped; every other property and child node is kept as written in
x-usageon its object, and written back.completealso becomesvaluesFromor aformat, and an implicitclause— flags scoped to the operand that ends each repetition — becomes an argument withscopedOptions. Usage has no extension mechanism, so converting cmdspec to Usage loses what Usage cannot say; a converter MUST report what it dropped.
OpenCLI 0.1 leaves one thing open that a converter must decide: its arity.maximum has a default
of 1, and an absent maximum is also described as unlimited. This specification reads no arity as
exactly one value and an arity without maximum as unbounded, which is how OpenCLI’s published
examples use it.
The mappings are part of the reference implementation, @docspack/cmdspec, and are tested in both
directions on published documents.