From f43fd61df8366b0a80f8ee18198477406301a532 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:53:36 +0000 Subject: [PATCH 1/2] docs: mark go and rust frameworks experimental, document both MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The landing page tiles for the Rust and Go frameworks now carry an "experimental" pill, tooltips that say APIs may change, and links to new documentation sections instead of GitHub. The new sections cover each framework end to end, written against the go/*, derive, and spec PRs currently in flight as if landed: - /rust/: crates and cargo features, args and flags, subcommands and flatten, validation (groups, exclusive, delimiter, relations), help and errors, completions, and spec output via to_kdl() - /go/: usage generate go and the generated structs/Parse, the zero-allocation event parser, binding and typed values, help and error rendering, and completions — including the gaps generated Parse still has (overrides, help printing, completion wiring) Co-Authored-By: Claude Fable 5 --- docs/.vitepress/config.mts | 30 ++++++ docs/.vitepress/theme/UsageHero.vue | 18 ++-- docs/.vitepress/theme/custom.css | 8 +- docs/go/binding.md | 78 ++++++++++++++ docs/go/completions.md | 79 ++++++++++++++ docs/go/generated-code.md | 115 +++++++++++++++++++++ docs/go/help.md | 82 +++++++++++++++ docs/go/index.md | 139 +++++++++++++++++++++++++ docs/go/parser.md | 75 ++++++++++++++ docs/rust/args-and-flags.md | 154 +++++++++++++++++++++++++++ docs/rust/completions.md | 94 +++++++++++++++++ docs/rust/help.md | 77 ++++++++++++++ docs/rust/index.md | 155 ++++++++++++++++++++++++++++ docs/rust/spec.md | 94 +++++++++++++++++ docs/rust/subcommands.md | 152 +++++++++++++++++++++++++++ docs/rust/validation.md | 135 ++++++++++++++++++++++++ 16 files changed, 1473 insertions(+), 12 deletions(-) create mode 100644 docs/go/binding.md create mode 100644 docs/go/completions.md create mode 100644 docs/go/generated-code.md create mode 100644 docs/go/help.md create mode 100644 docs/go/index.md create mode 100644 docs/go/parser.md create mode 100644 docs/rust/args-and-flags.md create mode 100644 docs/rust/completions.md create mode 100644 docs/rust/help.md create mode 100644 docs/rust/index.md create mode 100644 docs/rust/spec.md create mode 100644 docs/rust/subcommands.md create mode 100644 docs/rust/validation.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c6cc4b137..0c636c283 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -46,12 +46,42 @@ export default defineConfig({ nav: [ { text: "Home", link: "/" }, { text: "Spec", link: "/spec/" }, + { + text: "Frameworks", + items: [ + { text: "Rust", link: "/rust/" }, + { text: "Go", link: "/go/" } + ] + }, { text: "CLI", link: "/cli/" }, { text: `v${latestVersion}`, link: "https://github.com/jdx/usage/releases" } ], sidebar: [ { text: "Contributing", link: "/contributing" }, + { + text: "Rust Framework", + link: "/rust/", + items: [ + { text: "Args and Flags", link: "/rust/args-and-flags" }, + { text: "Subcommands", link: "/rust/subcommands" }, + { text: "Validation", link: "/rust/validation" }, + { text: "Help and Errors", link: "/rust/help" }, + { text: "Completions", link: "/rust/completions" }, + { text: "Spec Output", link: "/rust/spec" } + ] + }, + { + text: "Go Framework", + link: "/go/", + items: [ + { text: "Generated Code", link: "/go/generated-code" }, + { text: "The Parser", link: "/go/parser" }, + { text: "Binding and Values", link: "/go/binding" }, + { text: "Help and Errors", link: "/go/help" }, + { text: "Completions", link: "/go/completions" } + ] + }, { text: "CLI", link: "/cli/", diff --git a/docs/.vitepress/theme/UsageHero.vue b/docs/.vitepress/theme/UsageHero.vue index 00f1f28f9..0c77c1016 100644 --- a/docs/.vitepress/theme/UsageHero.vue +++ b/docs/.vitepress/theme/UsageHero.vue @@ -23,37 +23,33 @@ const javascriptPath =

Usage is a toolkit for building command-line tools. Define your CLI's commands, flags, and args once in a KDL spec — get argument parsing, shell completions, - --help, docs, and manpages from that one definition. Reference + --help, docs, and manpages from that one definition. Experimental reference frameworks for Rust and Go build your CLI from the spec, with Python and JavaScript planned.

Frameworks

- + Rust + experimental
Rust framework -

Derive your CLI from Rust types — parsing, help, and completions generated from the usage spec.

+

Derive your CLI from Rust types — parsing, help, and completions generated from the usage spec. Experimental: APIs may change between releases.

- + Go + experimental
Go framework -

Build Go CLIs on the usage spec, with parsing behavior verified against the same conformance corpus as the Rust implementation.

+

Build Go CLIs on the usage spec, with parsing behavior verified against the same conformance corpus as the Rust implementation. Experimental: APIs may change between releases.

diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index a63b7a996..9900d6ab0 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -279,7 +279,8 @@ box-shadow: 0 0 14px rgba(185, 103, 255, 0.3); } -.usage-tile-soon-pill { +.usage-tile-soon-pill, +.usage-tile-experimental-pill { font-family: Orbitron, sans-serif; font-size: 0.55rem; font-weight: 700; @@ -292,6 +293,11 @@ margin-left: 0.15rem; } +.usage-tile-experimental-pill { + color: var(--vw-cyan); + border-color: var(--vw-cyan); +} + /* Tooltips */ .usage-tile-tooltip { position: absolute; diff --git a/docs/go/binding.md b/docs/go/binding.md new file mode 100644 index 000000000..8faee11aa --- /dev/null +++ b/docs/go/binding.md @@ -0,0 +1,78 @@ +# Binding and Values + +Generated `Parse` does everything on this page for you. It's documented separately because the +pieces are public — custom binding loops use them directly — and because the _rules_ matter even +when you never call the functions: they define what your users' command lines mean. + +## Resolution order + +Each flag or arg resolves **argv → env → default**, matching +[config resolution](/spec/resolution): + +```go +values, source := argv.Fill(Meta.Lookup(key), given, argv.LookupEnv) +``` + +`Source` tells you where the value came from: `FromArgv`, `FromEnv`, `FromDefault`, or `Unset`. +`Source.Given()` is true only for the first two — a default is a fallback, not something the +user said, which matters for relations (below). + +The env rules are precise and worth knowing: + +- an **empty env variable is set** — `EX_JOBS=` provides the value `""` +- an env value is **one token, never re-split** on whitespace or commas +- for a value-less (boolean) flag, `argv.EnvTruth` decides whether the variable sets it at all — + the allow-list is narrow (`1`, `true`, `True`, `TRUE`), so `yes`, `on`, and `TrUe` do **not** + set the flag; this matches usage-lib + +## Checks + +```go +if err := argv.Check(meta, values, occurrences); err != nil { /* … */ } +``` + +- `required` is asked first: a required variadic given nothing is _missing_, not _short_ +- `choices` are case-sensitive and every value is checked, not just the first +- `var_min` only fires when something was given — an absent optional variadic hasn't broken its + minimum +- `var_max` counts **occurrences**, so one occurrence bringing three values doesn't break + `var_max=1` + +## Relations + +```go +err := argv.CheckRelationships(Meta, selectedKeys, sourceOf) +winners := argv.ApplyOverrides(Meta, tokenOrder) +``` + +`conflicts`, `required_if`, and `required_unless` are judged with a deliberate asymmetry: a +defaulted value counts for the entry _being judged_ but not for the _partners judging it_ — so a +flag with a default doesn't conflict with everything anyone types. + +`ApplyOverrides` implements last-one-wins on token order, symmetric regardless of which flag +declared the override, and runs _before_ fallbacks so a losing flag isn't refilled from env or a +default. Remember: [generated `Parse` does not call it](/go/generated-code#what-parse-enforces). + +## Typed values + +Generated struct fields are `string`/`[]string` — a spec names a value, it doesn't type it. +Conversions are explicit, and every failure is a `*argv.Error` with `CodeInvalidValue` carrying +the entry's name, the offending text, and a human phrase for what was expected: + +```go +n, err := argv.Int("jobs", cli.Jobs) // int64 — "a whole number" +u, err := argv.Uint("retries", cli.Retries) // uint64 — "a whole number, not negative" +f, err := argv.Float("ratio", cli.Ratio) // float64 — "a number" +b, err := argv.Bool("color", cli.Color) // bool — "true or false" +d, err := argv.Duration("wait", cli.Wait) // time.Duration — "a duration such as 30s or 1h30m" + +ports, err := argv.Each("ports", cli.Ports, argv.Int) // []int64, stops at the first bad value +``` + +Two sharp edges are deliberate: + +- **Nothing is trimmed.** `" 8 "` is refused, exactly as `" 8 ".parse::()` is in Rust — the + same spec means the same thing in both implementations. +- **`Bool` is wider than `EnvTruth` on purpose.** `Bool` accepts Go's spellings (`1`, `t`, `T`, + `true`, `TRUE`, `True` and the false counterparts) for a value someone typed; `EnvTruth` stays + on usage-lib's narrow list for deciding whether an env var sets a value-less flag. diff --git a/docs/go/completions.md b/docs/go/completions.md new file mode 100644 index 000000000..4327d933a --- /dev/null +++ b/docs/go/completions.md @@ -0,0 +1,79 @@ +# Completions + +The Go runtime answers the question every completion request boils down to — _what could go +where the cursor is?_ — from the same tables the parser runs on, so completions can never +disagree with the grammar. + +## Position and candidates + +```go +pos := argv.Walk(mycli.Root, wordsBeforeCursor) +candidates := argv.Candidates(pos, partialWord, mycli.HelpText, mycli.Meta) +``` + +`Walk` treats parse errors as _positions_, not failures — an unfinished command line is the +whole point. The `Position` tells you where the cursor stands: + +```go +type Position struct { + Cmd *argv.Command // the command in scope + Chain []*argv.Command // root → here + FlagsPossible bool // false past `--` + AwaitingValue *argv.Flag // cursor is inside this flag's value + NextArg *argv.Arg // the positional that would bind next + HelpTopic bool // after `help`: completing a topic, not a command to run +} +``` + +`Candidates` answers by asking the parser's own scope rules, never by re-deriving them: + +- subcommands and their visible aliases; hidden commands are never offered +- flags in scope — a global inside a subcommand yes, a non-global root flag no, and masking is + per spelling, matching help and the parser +- negation spellings (`--no-color`) as first-class candidates +- a flag awaiting its value takes the position entirely: its `choices` and nothing else +- the pending positional's `choices` — unless it demands a `--` that hasn't been typed +- nothing flag-shaped past a `--` + +Filtering by the partial word happens here, so every shell agrees on what matches. Each +candidate carries a `Describe` string for shells that display descriptions. + +## Speaking each shell's dialect + +```go +out := argv.RenderAnswer(argv.Answer{Candidates: candidates}, argv.Zsh) +``` + +`RenderAnswer` writes one line per candidate, in the format each shell reads: bash gets the +value alone; zsh gets display, description, and a quoted insert text (what it shows and what it +types differ); fish, nu, and PowerShell get value-tab-description. Values and descriptions are +sanitized so a candidate can never rearrange the protocol and make the shell insert something +nobody offered. + +An `Answer` can also request the shell's native file or directory completion: + +```go +argv.RenderAnswer(argv.Answer{Files: argv.AnyFile}, shell) // or argv.Dirs +``` + +## Wiring it up + +The protocol is the same one the Rust framework's generated shell scripts speak: the script +calls your binary back with + +``` + __complete_word__ --shell --line "" +``` + +Unlike the Rust framework — which intercepts `__complete_word__` automatically — the Go side +leaves the wiring to you today: + +1. **Recognize the hidden subcommand** before normal parsing (check `args[0]`). +2. **Split the line** into completed words plus the partial word under the cursor. +3. Call `Walk` → `Candidates` → `RenderAnswer` and print the result. +4. **Generate the install scripts** from your spec with the Rust CLI: + `usage g completion bash mycli --file mycli.usage.kdl` (and zsh/fish/…). + +Also not carried into the generated tables yet: spec-level `complete` run-scripts (only +`choices` are known to `Candidates`) and `value_hint` (derive an `Answer.Files` request +yourself where you want path completion). diff --git a/docs/go/generated-code.md b/docs/go/generated-code.md new file mode 100644 index 000000000..c3609ede8 --- /dev/null +++ b/docs/go/generated-code.md @@ -0,0 +1,115 @@ +# Generated Code + +`usage generate go` lowers a KDL spec into one Go file. The output is `gofmt`-clean and carries +the standard `// Code generated … DO NOT EDIT.` header. + +```bash +usage generate go -f mycli.usage.kdl -o tables.go -p mycli +``` + +| Flag | Meaning | +| ---------------------- | -------------------------------------------------------------------- | +| `-f --file ` | the KDL spec (`-` for stdin) | +| `--spec ` | a raw spec string instead of a file | +| `-o --out-file ` | output path (`-` for stdout) | +| `-p --package ` | package clause; defaults to the spec's `bin` made into an identifier | + +## What the file exports + +```go +const Version = "1.2.3" // only when the spec declares a version + +const ( // one key per command, flag, and argument + CmdRoot uint64 = 1 + FlagVerbose uint64 = 2 + ArgFile uint64 = 3 + CmdInstall uint64 = 4 + // … +) + +var Root *argv.Command // the hot parse table +var Meta argv.Metadata // validation metadata (required, choices, env, defaults, relations) +var HelpText argv.HelpTable // help text per entry +var HelpMeta argv.HelpSpec // root-level page furniture (name, bin, version, about) + +type Cli struct { /* … */ } // one struct for the root +type InstallCmd struct { /* … */ } // and one per command + +func Parse(args []string) (*Cli, error) +``` + +The three tables are separate on purpose: reference only `Root` and the linker drops the +validation metadata and help text. On mise's spec that's the difference between a 2.60MB and a +2.82MB contribution to the binary. Dispatch on the key constants, never on `Name` strings — a +rename in the spec then fails to compile instead of silently misrouting. + +## The structs + +For the spec on the [intro page](/go/): + +```go +// Cli is the whole command line. +type Cli struct { + Verbose bool // FlagVerbose + Jobs string // FlagJobs + File string // ArgFile + Install *InstallCmd // CmdInstall +} + +// InstallCmd is `install`. +type InstallCmd struct { + Force bool // FlagInstallForce + Pkg string // ArgInstallPkg +} +``` + +- The root struct is always `Cli`; a subcommand's is the Pascal-cased path plus `Cmd` + (`config ls` → `ConfigLsCmd`). +- Subcommands are pointers, and at most one per level is non-nil — that's how you tell which + path was taken. +- Field types: `count` flags → `int`; value-less flags → `bool`; `var` flags/args → `[]string`; + everything else → `string`. There is no type inference from the spec — a spec says what a + value is _called_, never what type it is. Convert with the + [typed helpers](/go/binding#typed-values). +- A flag and a command sharing a name are disambiguated by kind: a `--shell` flag beside a + `shell` command yields fields `Shell` and `ShellCmd`, not `Shell2`. + +## What `Parse` enforces + +`Parse` walks the events, fills the structs, then — for the commands the words actually +selected — applies fallbacks and checks: + +1. values resolve **argv → env → default**, per entry +2. `required`, `choices`, `var_min`/`var_max` are checked +3. `conflicts`, `required_if`, `required_unless` are checked across the selected commands + +A value-less flag set from an env var goes through `argv.EnvTruth` (usage-lib's narrow +allow-list: `1`, `true`, `True`, `TRUE`); a `default` on one compares against the literal +`"true"`. `count` fields are never filled from env or defaults — a count is occurrences, and +only the command line has those. A `default_subcommand` routes in the parser, so the defaulted +command's struct is filled with no caller involvement. + +Three things `Parse` deliberately does **not** do: + +- **`overrides` is not applied.** If your spec uses it, call `argv.ApplyOverrides` yourself. +- **Help and version are not printed** — they come back as `*argv.Error` with `CodeHelp` / + `CodeVersion` for you to render ([Help and errors](/go/help)). +- **No chain comes back with an error.** The renderers want the command chain; recover it with + `argv.Walk(Root, args)`, which returns the chain even for lines that failed to parse. + +## Using it against a real spec + +From the tests over mise's actual 211-command spec: + +```go +cli, err := mise.Parse([]string{"use", "-g", "node@20"}) +// cli.Use != nil; cli.Use.Global == true; cli.Use.ToolVersion == []string{"node@20"} +// cli.Config == nil — a command nobody ran is nil + +cli, _ = mise.Parse([]string{"tasks", "run", "build", "extra", "--", "--verbose"}) +run := cli.Tasks.Run +// run.Task == "build"; run.Args == []string{"extra"}; run.ArgsLast == []string{"--verbose"} + +_, err = mise.Parse([]string{"--log-level", "chatty"}) +e := err.(*argv.Error) // e.Code == argv.CodeInvalidChoice +``` diff --git a/docs/go/help.md b/docs/go/help.md new file mode 100644 index 000000000..414a8927b --- /dev/null +++ b/docs/go/help.md @@ -0,0 +1,82 @@ +# Help and Errors + +## Help pages + +Three renderers cover the usage line, the `-h` page, and the `--help` page: + +```go +argv.UsageLine(path, cmd, HelpText) // "mise [FLAGS] [TASK] " +argv.ShortHelp(HelpMeta, path, chain, HelpText) // the -h page +argv.LongHelp(HelpMeta, path, chain, HelpText) // the --help page +``` + +`path` is the command as invoked, binary first (`[]string{"mise", "config", "ls"}`); `chain` is +the `*argv.Command` chain from the root to the command (`argv.Walk` returns it, even for lines +that failed to parse). A rendered page: + +``` +List config files currently in use + +Usage: mise config ls [FLAGS] + +Flags: + -J, --json Output in JSON format + -h, --help Print help + +Global flags: + -C, --cd Change directory before running command +``` + +The output is not merely similar to the reference implementation's — all 211 of mise's usage +lines, `-h` pages, and `--help` pages are compared **byte for byte** against usage-lib's +rendering in CI. Layout details you get for free: sections in canonical order, commands sorted +with `[aliases: …]` shown for visible aliases, `help_heading` groups (first-seen order, unheaded +entries first), a 4-column short-flag gutter, required entries in angle brackets, `[env: X]` and +default annotations, and the long page wrapped at a fixed 80 columns. + +The short page appends `[choices]`, `[env: X]`, and (for arguments) `(default: …)` inline; the +long page gives each its own line and prefers `long_help` over `help`. Examples declared on the +root are inherited by commands that declare none. + +One rule is load-bearing: a page only advertises a flag spelling where that flag is the one that +would _bind_ it. Masking is per spelling — a subcommand redeclaring `--jobs` leaves an inherited +`-j` advertised if nothing claims it — and matches the parser exactly. + +## Rendering failures + +```go +msg := argv.Render(err, path, chain, HelpText) +``` + +The shape is clap's, which your users have seen before: + +``` +error: unknown flag `--wat` + +Usage: ex run [-f --force] + +For more information, try `--help`. +``` + +- The usage line names the command the user was **in**, not the program. +- `CodeHelp` and `CodeVersion` render as the empty string — print the page or version instead. +- Every error code renders something specific. `missing_flag_value` names the likeliest cause + and the escape hatch in the flag's actual spelling: ``missing value for `--jobs` (a value +beginning with `-` has to be attached: `--jobs=-x`)``. `invalid_choice` appends + `(expected one of: bash, zsh)`; the variadic codes pluralize correctly; `conflicting_flags` + names both sides. +- Anything quoted back to the user — tokens, unexpected arguments, rejected values — has control + characters escaped, so a hostile argv can't smuggle escape sequences to the terminal. + +The error type itself is small enough to use directly: + +```go +type Error struct { + Code Code // CodeUnknownFlag, CodeMissingRequiredFlag, CodeInvalidChoice, … + // plus the specifics: Token, Name, Choices, Bound, Got, Value, Want, Cmd, Long, … +} +``` + +`Error()` (the `error` interface) is a bare one-liner; `Render` is the version for humans. The +`Code` names are stable strings shared with the conformance corpus (`unknown_flag`, +`invalid_choice`, `var_too_many`, …), so tests can assert on classes rather than message text. diff --git a/docs/go/index.md b/docs/go/index.md new file mode 100644 index 000000000..dc08e7a20 --- /dev/null +++ b/docs/go/index.md @@ -0,0 +1,139 @@ +# Go Framework + +::: warning Experimental +The Go framework is experimental. Its parsing behavior is verified against the same conformance +corpus as the Rust implementation, but APIs may still change between releases. +::: + +The Go framework builds your CLI from a usage spec — but unlike most Go CLI libraries, your +shipped binary never parses the spec. `usage generate go` lowers the KDL into plain Go tables, +typed structs, and a `Parse` function at build time. The result: + +- **Zero dependencies.** The module is `github.com/jdx/usage/go` and imports nothing but the + standard library. +- **Zero-allocation parsing.** A parse allocates nothing, on success and failure paths alike — + roughly 57–110ns per parse on mise's real 211-command spec. +- **Linker-friendly.** Parse tables, validation metadata, and help text are three separate + tables; the linker drops the ones you don't reference. No `init` functions. +- **One source of truth.** The same KDL spec generates your completions, docs, and manpages. + +## Quick start + +Write a spec: + +```kdl +name "ex" +bin "ex" +version "1.0.0" +flag "-v --verbose" global=#true help="be loud" +flag "-j --jobs " help="how many jobs" +arg "" help="the file to process" +cmd "install" help="install a tool" { + alias "i" + flag "-f --force" + arg "" +} +``` + +Generate the Go code: + +```go +//go:generate usage generate go -f ex.usage.kdl -o tables.go -p ex +``` + +Parse: + +```go +package main + +import ( + "fmt" + "os" + + "github.com/jdx/usage/go/argv" +) + +func main() { + cli, err := ex.Parse(os.Args[1:]) + if err != nil { + exit(err.(*argv.Error)) + } + if cli.Install != nil { + install(cli.Install.Pkg, cli.Install.Force, cli.Verbose) + return + } + process(cli.File) +} +``` + +`Parse` returns a typed struct per command — `cli.Install` is `nil` unless `install` (or its +alias `i`) was invoked — with flags bound, `env`/`default` fallbacks applied, and `required`, +`choices`, `var_min`/`var_max`, and flag relations enforced. + +## Handling help, version, and failures + +Unlike the Rust framework's `parse()`, the generated Go `Parse` never prints or exits — help and +version requests come back as errors with `Code` set, and rendering is yours to invoke. The +standard exit function looks like this: + +```go +func exit(e *argv.Error) { + pos := argv.Walk(ex.Root, os.Args[1:]) + path := []string{"ex"} + for _, c := range pos.Chain[1:] { + path = append(path, c.Name) + } + switch e.Code { + case argv.CodeHelp: + if e.Long { + fmt.Print(argv.LongHelp(ex.HelpMeta, path, pos.Chain, ex.HelpText)) + } else { + fmt.Print(argv.ShortHelp(ex.HelpMeta, path, pos.Chain, ex.HelpText)) + } + os.Exit(0) + case argv.CodeVersion: + fmt.Println("ex " + ex.Version) + os.Exit(0) + default: + fmt.Fprint(os.Stderr, argv.Render(e, path, pos.Chain, ex.HelpText)) + os.Exit(2) + } +} +``` + +The rendered pages match usage-lib's byte for byte, and the failure messages are clap-shaped — +see [Help and errors](/go/help). + +## Why generation instead of a runtime spec? + +The Go module has no KDL parser, on purpose. Lowering a spec is `usage-cli`'s job, done once at +build time; the shipped binary carries tables the linker can lay out as data. Building tables at +runtime is not supported — generation is the only path, and the point. + +Everything is verified against the reference implementation: a shared JSON conformance corpus +(all vectors passing) covers the parsing grammar, and all 211 of mise's usage lines, `-h` pages, +and `--help` pages are compared byte-for-byte against usage-lib's rendering in CI. + +## Where to go next + +- [Generated code](/go/generated-code) — what `usage generate go` emits and what `Parse` does +- [The parser](/go/parser) — the low-level zero-allocation event API +- [Binding and values](/go/binding) — env/default resolution, validation, typed conversions +- [Help and errors](/go/help) — rendering `-h`/`--help` pages and failures +- [Completions](/go/completions) — answering shell completion requests + +## Current limitations + +Worth knowing before you commit: + +- **`overrides` is not enforced by generated `Parse`.** `conflicts`, `required_if`, and + `required_unless` are; a spec relying on last-one-wins `overrides` semantics needs to call + `argv.ApplyOverrides` itself. +- **Fields are `string`, `bool`, `[]string`, or `int` (for counts).** A spec says what a value is + called, never what type it is — convert with [`argv.Int`, `argv.Duration`, etc.](/go/binding#typed-values) +- **`complete` scripts, `config` nodes, `group`, `value_hint`, and `mount` are not carried into + the generated tables.** Completions know `choices`; config resolution is not implemented. +- **Completion shell scripts come from the Rust side.** The Go runtime answers completion + requests over the same protocol, but you wire up the hidden subcommand yourself — see + [Completions](/go/completions). +- Command trees deeper than 16 levels are rejected (`CodeTooDeep`); short flags must be ASCII. diff --git a/docs/go/parser.md b/docs/go/parser.md new file mode 100644 index 000000000..1bf922478 --- /dev/null +++ b/docs/go/parser.md @@ -0,0 +1,75 @@ +# The Parser + +Generated `Parse` is the front door, but the event-level API underneath is public and stable — +use it when you need custom binding, a REPL, or completion positions. + +```go +p := argv.New(mycli.Root, os.Args[1:]) +for p.Next() { + switch ev := p.Event(); ev.Kind { + case argv.KindCommand: + // ev.Command was selected + case argv.KindFlag: + // ev.Flag; ev.Value if ev.HasValue; ev.Negated for --no-* spellings + case argv.KindArg: + // ev.Value filled ev.Arg + } +} +if err := p.Err(); err != nil { + // a failure, or a help/version request +} +``` + +`New` returns the parser by value — it lives on the stack, and a parse performs **zero heap +allocations**, on success and failure paths alike (pinned by a `testing.AllocsPerRun` test). +An error is terminal: events already yielded are not a partial result; discard the attempt. + +`Event.Value` aliases the argv strings — it's the raw OS bytes and is **not guaranteed to be +valid UTF-8**. Validate where you build your target type. + +## Grammar + +The behavior below is pinned vector-by-vector in the shared conformance corpus, so it's the same +grammar usage-lib and the Rust framework parse: + +| Input | Result | +| ------------------------------------- | --------------------------------------------------------------------------- | +| `--jobs=8`, `--jobs 8`, `-j8`, `-j=8` | the flag with value `8` | +| `--jobs=` | empty string **is** a value (`HasValue` true) | +| `--jobs=a=b` | value `a=b` — only the first `=` splits | +| `-fv` | a short-flag bundle: `force`, then `verbose` | +| `-fj8` | a value-taking short ends the bundle: `force`, `jobs=8` | +| `--no-color` | the `color` flag with `Negated` set | +| `--jobs -1` | negative numbers are the one detached dash-token accepted as a value | +| `-- --force` | everything after `--` is positional | +| `-` | a bare `-` binds as a value where it was typed | +| `install` / `i` | command descent (aliases included) | +| `--force install` | a subcommand's flags are not in scope above it | +| `other install` | only the descent position routes — `install` here is a value | +| `--include a b` | a variadic flag collects values until a flag-like token | +| `--for` | no abbreviation inference — unknown flags fall through as values by default | + +Unknown flags are governed per command by the spec's `unknown_flags`: the default `"value"` lets +the token fall through to the positionals (specs often wrap someone else's flags); `"error"` +rejects it — and rejects a bundle like `-fz` **whole**, with no partial `-f` event. + +`--help`/`-h` arrive as ordinary flag events pointing at the package-level `argv.HelpShort` / +`argv.HelpLong` flags. The bare word `help` is a question rather than a command: it stops the +parse with `CodeHelp` and `Error.Cmd` set to the command asked about. `Error.Long` distinguishes +`--help` from `-h`. + +Counting flags need nothing from the parser — each occurrence is its own event, and the caller +tallies (generated code does `field++`). + +## Limits + +- `argv.MaxDepth` is 16; deeper command trees fail with `CodeTooDeep`. (mise is 4 deep.) +- `Flag.Shorts` must be ASCII — a non-ASCII short can never match, and the rest of a bundle + after a value-taking short would begin mid-character. + +## Hand-written tables + +Tables are plain data (`argv.Command`, `argv.Flag`, `argv.Arg`), so writing them by hand is +supported — but the generator is the intended path. If you do write them by hand: keys must be +dense starting from 1, since `Metadata` and `HelpTable` are indexed by key and `Lookup` returns +`nil` on drift rather than a neighbor. diff --git a/docs/rust/args-and-flags.md b/docs/rust/args-and-flags.md new file mode 100644 index 000000000..74d69720e --- /dev/null +++ b/docs/rust/args-and-flags.md @@ -0,0 +1,154 @@ +# Args and Flags + +A field on a `#[derive(Cli)]` or `#[derive(Args)]` struct becomes a flag when it carries `long` +or `short`, and a positional argument otherwise (or explicitly with `#[usage(arg)]`). + +```rust +#[derive(Cli)] +#[usage(bin = "ex")] +struct Cli { + /// User to run as + #[usage(short = 'u', long)] + user: Option, // flag: -u, --user + + /// The directory to use + dir: String, // required positional: + + /// The files to read + files: Vec, // variadic positional: [files]... +} +``` + +## Types drive cardinality + +Whether something is required, optional, or repeatable is read off the field's type — the type +has nowhere to put "absent", so `T` means required: + +| Field type | Meaning | +| ---------------------- | --------------------------------------- | +| `T` | one value, **required** | +| `Option` | one value or nothing | +| `Vec` | several values; empty when none arrived | +| `Option>` | several values; `None` when never given | +| `bool` | a switch | +| `u8`…`usize` + `count` | occurrence count (`-vvv` → `3`) | + +Values are built with `FromStr`, so `PathBuf`, `usize`, `IpAddr`, and your own types all work. +The `FromStr` error type must implement `Display` (a compile error names the type otherwise); +a conversion failure at runtime becomes `Error::InvalidValue { name, value, reason }`. + +A `Vec` flag is repeatable (`var` in spec terms) automatically. Two related attributes cover the +other shapes: + +- `var` — makes a _single-value_ flag repeatable where the last occurrence wins +- `variadic` — one occurrence greedily takes values: `--include a b c` + +`var` and `variadic` together is a compile error. A **required** `Vec` is the one place +required-ness is declared rather than inferred: `#[usage(arg, required)]`. + +## Field attributes + +```rust +#[usage(short = 'j', long, env = "EX_JOBS", default = "4")] +jobs: Option, +``` + +| Attribute | Effect | +| --------------------------------------- | --------------------------------------------------------------------------------------- | +| `long` / `long = "name"` | `--name` flag (defaults to the kebab-cased field name) | +| `short` / `short = 'x'` | `-x` flag (defaults to the field name's first letter) | +| `name = "…"` | Override the arg/flag name used in help and the spec | +| `arg` | Force the field to be a positional argument | +| `env = "VAR"` | Fall back to this environment variable when the flag/arg wasn't given | +| `default = "…"` | Fall back to this value (repeatable for `Vec` fields) | +| `negate = "--no-x"` | A negation flag that sets a `bool` back to false | +| `count` | Count occurrences into an integer field | +| `global` | Usable on any subcommand below this one | +| `var` / `variadic` | Repeatable / greedy multi-value (see above) | +| `var_min = n` / `var_max = n` | Bounds on how many values a `Vec` may hold | +| `choices("a", "b")` | Restrict values to a fixed set | +| `value_enum` | Take choices from a `#[derive(ValueEnum)]` type | +| `delimiter = ','` | Split one word into several values ([Validation](/rust/validation#delimiters)) | +| `group = "name"` | Join a flag group ([Validation](/rust/validation#groups)) | +| `exclusive` | Must be given alone ([Validation](/rust/validation#exclusive-flags)) | +| `conflicts(…)` / `requires(…)` | Relations to other flags ([Validation](/rust/validation)) | +| `overrides(…)` | Later occurrence silently overrides the named flag | +| `required_if(…)` / `required_unless(…)` | Conditional required-ness | +| `complete = my_fn` | Custom completion function ([Completions](/rust/completions)) | +| `value_hint = ValueHint::FilePath` | Complete values as paths (`FilePath`, `DirPath`, `AnyPath`) | +| `value_name = "…"` | The placeholder shown in help (`--file `) | +| `help = "…"` / `long_help = "…"` | Help text (doc comments are usually nicer) | +| `help_heading = "…"` | Group the entry under a heading in help output | +| `hide` | Omit from help, docs, and completions | +| `required` | Explicit required-ness (for `Vec` fields) | +| `value_optional` | Mark the value optional in help (help-only; the parser still wants one) | +| `double_dash = "…"` | `"optional"`, `"required"`, `"preserve"`, or `"automatic"` `--` handling | +| `effect = "…"` | `"read"`, `"write"`, or `"destructive"` — see [command effects](/spec/#command-effects) | +| `setting = "key"` | Bind to a config setting (generates `parse_from_with_settings`) | +| `verbatim_doc_comment` | Keep the doc comment's line breaks in help | + +Flag relations (`conflicts`, `requires`, `overrides`, `required_if`, `required_unless`) name +their target the way the KDL spec does — `"--long"` or `"-s"`, one value or a list: + +```rust +#[usage(long, conflicts("--file", "--url"))] +stdin: bool, +``` + +Naming a flag that doesn't exist on the command is a **compile error**, not a runtime surprise. +Relations are flag-to-flag only; a positional cannot carry one. + +## Resolution order + +After argv is parsed, each field resolves in this order — matching +[config resolution](/spec/resolution) for the spec at large: + +1. the value given on the command line +2. the `env` variable, if set +3. the `default`, if declared + +Then validation runs: required-ness (skipped for anything a default or env var filled), +`choices`, and `var_min`/`var_max`. Only the command that actually ran is judged — a required +flag on a sibling subcommand you didn't invoke costs nothing. + +## Global flags + +A `global` flag declared on a parent is accepted anywhere below it: + +```rust +/// Say yes to everything +#[usage(long, short = 'y', global)] +yes: bool, +``` + +A global flag may be given **once per command level**, with the innermost occurrence winning — +`mycli -y install -y` works, matching clap. Giving it twice at the _same_ level is still a +`DuplicateFlag` error: `mycli -y -y` is refused. + +## Container attributes + +On the root `#[derive(Cli)]` struct: + +| Attribute | Effect | +| ----------------------------------- | -------------------------------------------------------------- | +| `bin = "…"` | The binary name (used in help and the spec) | +| `name = "…"` | A friendly display name | +| `version` / `version = "…"` | Enable `--version`/`-V`; bare form uses `CARGO_PKG_VERSION` | +| `about` / `long_about` | Description (doc comments work too) | +| `usage = "…"` | Verbatim synopsis line(s), replacing the generated one | +| `before_help` / `after_help` | Extra text around the help page (`*_long_help` variants too) | +| `unknown_flags = "value"\|"error"` | Treat unknown flags as values instead of errors | +| `default_subcommand = "run"` | Command to assume when argv names none | +| `completion` | Generate completion support ([Completions](/rust/completions)) | +| `settings` | Generate config-settings bindings | +| `min_usage_version = "…"` | Declare the minimum usage version the spec needs | +| `group("name", required, multiple)` | Declare a flag group ([Validation](/rust/validation#groups)) | + +On a `#[derive(Args)]` struct (refused on the root): + +| Attribute | Effect | +| ----------------------- | ---------------------------------------------------------------------------- | +| `alias = "…"` | Alternative command name (`alias_hidden` hides it from help) | +| `mount = "…"` | Mount a subprocess-provided spec for completions ([Spec output](/rust/spec)) | +| `restart_token = ":::"` | Token that restarts parsing (for wrapper CLIs) | +| `effect = "…"` | The command's [effect classification](/spec/#command-effects) | diff --git a/docs/rust/completions.md b/docs/rust/completions.md new file mode 100644 index 000000000..b44badc5d --- /dev/null +++ b/docs/rust/completions.md @@ -0,0 +1,94 @@ +# Completions + +Completion support is opt-in: add `completion` to the root attribute and enable the +`completions` cargo feature (forgetting the feature is a compile error that names it): + +```toml +[dependencies] +usage = { package = "usage-rs", version = "5", features = ["completions"] } +``` + +```rust +#[derive(Cli)] +#[usage(bin = "ex", completion)] +struct Ex { /* … */ } +``` + +This generates two methods and wires the runtime protocol into `parse()`: + +```rust +// the script a user installs into their shell +pub fn completion_script(shell: usage::complete::Shell) -> String; + +// answer a runtime completion request, if argv is one +pub fn completion_request(argv: &[OsString]) -> Option; +``` + +`Shell` covers `Bash`, `Zsh`, `Fish`, `Nu`, and `PowerShell`. + +## How it works + +The installed script calls your binary back at completion time with a hidden +`__complete_word__` request describing the line and cursor. The request is recognized _before_ +any parsing, so it never appears in your grammar, help, or spec. `parse()` intercepts it +automatically; with `parse_from`, call `completion_request` first and print whatever it returns. + +A typical way to expose the scripts: + +```rust +#[derive(Args)] +struct Completion { + /// Which shell to generate for + #[usage(long, value_enum)] + shell: Shell, +} + +// in your run function: +print!("{}", Ex::completion_script(cli.completion.shell.into())); +``` + +Candidates come from the same tables the parser uses: subcommands and their visible aliases, +flags in scope at the cursor (globals included, hidden entries excluded), `choices` and +`ValueEnum` words for a pending value, and negation spellings. + +## Completing values + +Three ways to say what a value can be: + +```rust +// a fixed set of words +#[usage(long, choices("json", "table"))] +format: Option, + +// paths — the shell's native file completion takes over +#[usage(long, value_hint = usage::ValueHint::FilePath)] +file: Option, + +// anything you can compute +#[usage(arg, name = "TASK", complete = tasks_in_file)] +task: Option, +``` + +`ValueHint` (`FilePath`, `DirPath`, `AnyPath`) answers with the shell's own file/directory +completion and also emits `complete "file" type="path"` into the KDL, so external consumers of +the spec give the same answer. + +A custom completer is a plain function, referenced by _path_ — a typo is a compile error, not a +silent dead completer: + +```rust +fn tasks_in_file( + partial: &::Partial, + _ctx: &usage::complete::CompleteCtx<'_>, +) -> Vec> { + let file = partial.file.as_deref().unwrap_or("tasks.toml"); + read_tasks(file) + .map(|t| usage::complete::Candidate::described(t.name, t.about)) + .collect() +} +``` + +The first parameter is the _partial parse_ of the completer's own command — flags the user has +already typed are available, so a `--file` flag can steer what gets completed. Build candidates +with `Candidate::new(value)` or `Candidate::described(value, description)`; shells that display +descriptions (zsh, fish) show them, shells that don't get the value alone. diff --git a/docs/rust/help.md b/docs/rust/help.md new file mode 100644 index 000000000..caa413bfb --- /dev/null +++ b/docs/rust/help.md @@ -0,0 +1,77 @@ +# Help, Version, and Errors + +## Help + +`-h` and `--help` are supplied by the parser — you never declare them. They aren't written into +the spec either, so the help page never disagrees with the spec about what exists. If your CLI +declares its own `--help`, your declaration wins for that spelling. + +`-h` renders the short page, `--help` the long page: the first paragraph of each doc comment +versus the whole comment, `long_help` over `help`, `long_about` over `about`. + +With `parse()`, help is handled for you — printed to stdout, exit `0`. With `parse_from`, a help +request comes back as an _error_, because a parse that stopped to print help has not produced a +value (clap models it the same way): + +```rust +use usage::{help, Error}; + +match Ex::parse_from(&argv) { + Ok(cli) => run(cli), + Err(Error::Help { cmd, long }) => { + print!("{}", help::render(Ex::spec(), cmd, long).unwrap()); + } + Err(Error::Version) => { + println!("ex {}", env!("CARGO_PKG_VERSION")); + } + Err(err) => { + eprint!("{}", usage::render_failure(Ex::spec(), &argv, &err)); + std::process::exit(2); + } +} +``` + +`Error` is `#[non_exhaustive]` — always keep a fallback arm. + +### Customizing the page + +- `usage = "…"` on the root replaces the generated synopsis line(s) verbatim. +- `before_help`, `after_help`, `before_long_help`, `after_long_help` add text around the page — + `after_long_help` is the conventional home for an Examples section. +- `help_heading` on a field groups it under a heading. +- `hide` removes an entry from help, docs, and completions while still parsing. + +The rendered output matches what usage-lib renders from the same spec — the two renderers are +held to identical output over mise's 211 command pages in CI. + +## Version + +Declaring `version` (or bare `version`, which reads `CARGO_PKG_VERSION`) gives the root command +`--version` and `-V`. Neither is listed in help. If your CLI declares its own `--version` or +`-V`, your spelling wins and the other still answers — where clap panics at startup for the +same collision. + +`parse()` prints `{bin} {version}` and exits `0`. + +## Errors + +`parse_from` returns `usage::Error`, which distinguishes every failure the grammar can produce: +`UnknownFlag`, `MissingFlagValue`, `UnexpectedArg`, `MissingRequired`, `DuplicateFlag`, +`InvalidChoice`, `InvalidValue`, `VarTooFew`/`VarTooMany`, `ConflictingFlags`, `MissingGroup`, +`MissingSubcommand`, `ArgRequiresDoubleDash`, and more — plus `Help` and `Version` as described +above. + +`render_failure(spec, argv, &err)` turns any of them into the message users see. With the +`diagnostics` feature enabled the message is clap-shaped: + +``` +error: unexpected argument '--wat' found + +Usage: ex [OPTIONS] + +For more information, try '--help'. +``` + +Without `diagnostics`, it falls back to the `Debug` form of the error — fine for internal tools, +not what you want to ship. `parse()` prints the rendered failure to **stderr** and exits **2**, +clap's status, so scripts that check for it keep working. diff --git a/docs/rust/index.md b/docs/rust/index.md new file mode 100644 index 000000000..f06468b15 --- /dev/null +++ b/docs/rust/index.md @@ -0,0 +1,155 @@ +# Rust Framework + +::: warning Experimental +The Rust framework is experimental. It is complete enough that `usage-cli` itself is built with it, +but attribute names and APIs may still change between releases. +::: + +The Rust framework builds your CLI from Rust types. You declare commands, flags, and args as +structs and enums; a derive macro compiles that declaration into parse tables **and** a usage +spec. The same declaration that parses argv is the spec that generates your docs, manpages, and +shell completions. + +```rust +use usage::Cli; + +/// A tool that does things +#[derive(Cli)] +#[usage(bin = "ex", version = "1.0")] +struct Cli { + /// How many jobs to run at once + #[usage(short = 'j', long, env = "EX_JOBS", default = "4")] + jobs: Option, + + /// Print more + #[usage(short = 'v', long, count)] + verbose: u8, + + /// Colorize output + #[usage(long, negate = "--no-color", default = "true")] + color: bool, + + /// Files to process + files: Vec, +} + +fn main() { + let cli = Cli::parse(); + // cli.jobs, cli.verbose, cli.color, cli.files are ready to use +} +``` + +Doc comments are the help text: the first paragraph becomes the short help shown by `-h`, the +whole comment becomes the long help shown by `--help`. + +## Installation + +Add `usage-rs` to your `Cargo.toml`, aliased to `usage`: + +```toml +[dependencies] +usage = { package = "usage-rs", version = "5" } +``` + +The alias is supported directly — the derive resolves its runtime through the package name, so +depending on `usage-rs` under any name works. `usage-rs` is a facade over two crates you can also +use directly: + +| Crate | Role | +| -------------- | -------------------------------------------------------------------------- | +| `usage-rs` | The facade an application depends on; re-exports the whole runtime | +| `usage-derive` | The derive macros: `Cli`, `Args`, `Subcommands`, `ValueEnum` | +| `usage-argv` | The zero-allocation, zero-dependency runtime the derive emits code against | + +### Cargo features + +| Feature | Default | What it enables | +| ------------- | :-----: | ------------------------------------------------------------ | +| `spec` | ✅ | Spec metadata and `to_kdl()`; gates the derives | +| `help` | ✅ | `-h` / `--help` page rendering | +| `completions` | | Shell completion scripts and the runtime completion protocol | +| `diagnostics` | | clap-shaped error messages from `render_failure` | + +Two footguns worth knowing up front: + +- Without `diagnostics`, parse failures print as a `Debug`-formatted error rather than the + friendly clap-shaped message. Enable it for anything user-facing. +- `#[usage(completion)]` without the `completions` feature is a deliberate `compile_error!` that + tells you which feature to add. + +## Parse entry points + +`#[derive(Cli)]` generates these on your struct: + +```rust +// parse std::env::args; print help/version/errors and exit as appropriate +pub fn parse() -> Self; + +// parse the given argv; hand errors (including help/version requests) back to you +pub fn parse_from<'v>(argv: &'v [&'v OsStr]) -> Result>; + +// the static parse tables and spec metadata +pub fn command() -> &'static usage::Command<'static>; +pub fn spec() -> &'static usage::spec::Spec<'static>; + +// the usage spec as KDL +pub fn to_kdl() -> String; +``` + +`parse()` is the whole program shell: it prints the help page to stdout and exits `0` for +`-h`/`--help`, prints `{bin} {version}` and exits `0` for `--version`, and prints a rendered +failure to stderr and exits `2` — clap's exit status, so scripts that check for it keep working. +`parse_from` gives you the same machinery without the process control; see +[Help, version, and errors](/rust/help) for handling its `Err` variants. + +## One declaration, every artifact + +Because the derive also emits a usage spec, everything on this site that consumes a spec works +with your CLI. The pattern `usage-cli` itself ships is a hidden flag that prints the spec: + +```rust +#[usage(long, hide)] +usage_spec: bool, +``` + +```rust +if cli.usage_spec { + println!("{}", Cli::to_kdl().trim()); + return; +} +``` + +Then generate everything else from it: + +```bash +mycli --usage-spec > mycli.usage.kdl +usage g markdown -f mycli.usage.kdl --out-dir docs +usage g manpage -f mycli.usage.kdl > mycli.1 +usage g completion bash mycli --file mycli.usage.kdl +``` + +See [Spec output](/rust/spec) for the round-trip guarantees and what the emitted KDL looks like. + +## Where to go next + +- [Args and flags](/rust/args-and-flags) — field types, attributes, env vars, defaults +- [Subcommands](/rust/subcommands) — command enums, nesting, `flatten`, value enums +- [Validation](/rust/validation) — choices, groups, `exclusive`, `delimiter`, conflicts +- [Help, version, and errors](/rust/help) — what the parser renders and how to hook it +- [Completions](/rust/completions) — static scripts and runtime completion +- [Spec output](/rust/spec) — the emitted KDL and usage-cli integration + +## Current limitations + +The framework intentionally targets standard GNU-style CLIs, and a few clap features have no +equivalent yet: + +- `example` nodes exist in the spec format but cannot be declared from the derive — put an + Examples section in `after_long_help` instead (mise does this). +- `value_optional` affects help output only; the parser still requires a value for the flag. +- There is no per-field `value_parser`-style validation — values are built with `FromStr`, and a + conversion failure becomes an `InvalidValue` error. +- Prefix matching (`infer_long_args`) is suggested in error messages but never accepted. +- Non-UTF-8 argv values are reported precisely in errors rather than lossily replaced, but cannot + currently be _accepted_ into fields (the crates forbid the `unsafe` needed to reconstruct an + `OsString`). diff --git a/docs/rust/spec.md b/docs/rust/spec.md new file mode 100644 index 000000000..cc64ef5ee --- /dev/null +++ b/docs/rust/spec.md @@ -0,0 +1,94 @@ +# Spec Output + +`Cli::to_kdl()` writes a complete [usage spec](/spec/) from the same static metadata the parser +runs on. This is the bridge to the rest of the toolkit: markdown docs, manpages, completion +scripts for other consumers, SDK generation, and linting all consume that KDL. + +For the declarations shown across these pages, the emitted spec looks like: + +```kdl +name "ex" +bin "ex" +version "1.2.3" +about "does things" +flag "-j --jobs" help="how many jobs" global=#true help_heading="Performance" env="EX_JOBS" default="4" { + long_help "More about jobs.\nOn two lines." + arg "" +} +flag "--color" help="colorize output" negate="--no-color" default="true" +flag "-v --verbose" hide=#true count=#true +flag "--include" var=#true var_min=1 var_max=5 overrides="--exclude" { + arg "..." +} +group "input" "--file" "--url" "--stdin" required=#true +arg "[file]" help="the file" env="EX_FILE" default="a.txt" +cmd "install" help="install a tool" effect="write" { + alias "i" + alias "add" hide=#true + flag "-f --force" + arg "" +} +cmd "run" help="run a task" restart_token=":::" { + mount run="ex tasks --usage" + arg "[args]..." double_dash="preserve" +} +``` + +## Round-trip guarantee + +The emitted KDL parses with usage-lib and every property survives the trip — this is enforced by +the conformance suite. The test every adopter should write is one line: + +```rust +#[test] +fn spec_is_valid() { + let spec: usage::Spec = Cli::to_kdl().parse().unwrap(); + let _ = spec; +} +``` + +Beyond parsing, `to_kdl` asserts (in debug builds) that the tree is coherent: no duplicate keys, +no duplicate flag spellings across a `flatten` boundary, no duplicate group names, no unfillable +argument after an unbounded variadic. Those fire in your test, not on users. + +## Feeding usage-cli + +The pattern usage-cli itself ships is a hidden flag that prints the spec, so the binary is the +source of truth: + +```rust +#[usage(long, hide)] +usage_spec: bool, +``` + +```bash +mycli --usage-spec > mycli.usage.kdl + +usage g markdown -f mycli.usage.kdl --out-dir docs # markdown docs +usage g manpage -f mycli.usage.kdl > mycli.1 # man page +usage g completion bash mycli --file mycli.usage.kdl # completion script +usage g json -f mycli.usage.kdl # JSON form +usage lint -f mycli.usage.kdl # lint the spec +``` + +`min_usage_version = "…"` on the root is written first in the document, as the CLI's claim about +which usage consumers can read it. + +## What the parser does with the spec + +Nothing, at runtime. The derive compiles your declaration into static tables that usage-argv +parses and renders help from directly — no KDL is parsed when your CLI runs, and usage-lib is +not a dependency of your binary. The spec is the _export_ format. The two implementations are +held to identical behavior by a shared conformance corpus and by rendering all 211 of mise's +help pages through both. + +## What can't be expressed from the derive + +A few spec nodes have no derive attribute yet: + +- `example` nodes — declare examples in `after_long_help` instead +- `allow_hyphen_values` +- `forwards` / external subcommands + +If you need these today, maintain a KDL spec alongside the derive or generate docs from a +post-processed spec. diff --git a/docs/rust/subcommands.md b/docs/rust/subcommands.md new file mode 100644 index 000000000..c1ac2c9a9 --- /dev/null +++ b/docs/rust/subcommands.md @@ -0,0 +1,152 @@ +# Subcommands + +Subcommands are an enum. Each variant wraps a `#[derive(Args)]` struct (or nothing), and the +enum derives `Subcommands`: + +```rust +/// A tool that does things +#[derive(Cli)] +#[usage(bin = "ex", version = "1.0")] +struct Ex { + /// Say more + #[usage(short = 'v', long, global)] + verbose: bool, + + /// What to do + #[usage(subcommand)] + command: Option, +} + +#[derive(Subcommands)] +enum Commands { + /// Install a tool + Install(Install), + /// Run a task + #[usage(name = "run")] + RunTask(Run), +} + +#[derive(Args)] +struct Install { + /// Overwrite an existing install + #[usage(short = 'f', long)] + force: bool, + /// What to install + tools: Vec, +} +``` + +- `Option` makes the subcommand optional; a bare `Commands` field makes it + **required** (`subcommand_required` in the emitted spec). +- Variant names kebab-case into command names; override with `#[usage(name = "…")]`. +- A variant may box its struct — `Install(Box)` — with no semantic change. +- A **unit variant** is a command with nothing of its own; `name`, `alias`, `hide`, and + `effect` go directly on the variant. +- Nesting is unbounded in practice: an `Args` struct can carry its own + `#[usage(subcommand)]` field, up to a maximum depth of 16. + +Variant attributes: `name`, `alias`, `alias_hidden`, `hide`, `effect`, `help`, `long_help`, +`verbatim_doc_comment`. Aliases declared on the variant and on the `Args` struct are joined. + +Two variants wrapping the _same_ struct is a compile error — each command needs its own +declaration (two byte-identical structs in different modules are fine). + +## Default subcommand + +```rust +#[derive(Cli)] +#[usage(bin = "ex", default_subcommand = "run")] +struct Ex { /* … */ } +``` + +When argv selects no command, `run` is assumed. Naming a command that doesn't exist fails the +**build**, not the run. + +## Sharing declarations with `flatten` + +`#[usage(flatten)]` splices another struct's flags and args into a command, so two commands can +share a set of declarations: + +```rust +#[derive(Args)] +struct Listing { + /// Do not print a header + #[usage(long)] + no_header: bool, + /// Output format + #[usage(long, choices("json", "table"))] + format: Option, +} + +#[derive(Args)] +struct Config { + #[usage(long, short = 'f')] + file: Option, + + #[usage(flatten)] + listing: Listing, // config gets --no-header and --format too +} +``` + +The tables are joined at compile time and the emitted KDL lists the flags inline — a consumer of +the spec can't tell a flattened flag from a declared one. Groups and `exclusive` flags declared +on the flattened struct are enforced (and emitted) on the command that flattens them. + +A flattened struct may not declare subcommands; that's a compile-time error with an explanation. + +## Value enums + +For a flag or arg whose values are a fixed set of words, derive `ValueEnum` instead of listing +`choices` by hand: + +```rust +#[derive(usage::ValueEnum)] +enum Shell { + Bash, + Zsh, + #[usage(name = "pwsh")] + PowerShell, +} + +#[derive(Args)] +struct Completion { + /// Which shell to generate for + #[usage(long, value_enum)] + shell: Option, +} +``` + +Variant names kebab-case into the accepted words. The derive also implements `FromStr`, whose +error lists the valid words. One limitation: a single variant cannot be `cfg`-ed out (the word +list is a `const`) — put the `cfg` on the whole enum. + +## Mounts and restart tokens + +Two spec features for wrapper-style CLIs are declared on the `Args` struct: + +```rust +#[derive(Args)] +#[usage(mount = "ex tasks --usage", restart_token = ":::")] +struct Run { + /// Arguments passed through to the task + #[usage(double_dash = "preserve")] + args: Vec, +} +``` + +`mount` names a command that prints a spec for dynamically-defined subcommands (like mise +tasks); it is only consulted during completion — the cold path where running a subprocess is +affordable. `restart_token` lets one invocation contain several command lines +(`ex run build ::: test`). See the [spec reference](/spec/reference/cmd) for semantics. + +## Command effects + +A variant or `Args` struct can declare what running the command does to the world: + +```rust +#[derive(Args)] +#[usage(effect = "destructive")] +struct Uninstall { /* … */ } +``` + +See [command effects](/spec/#command-effects) for what consumers do with this. diff --git a/docs/rust/validation.md b/docs/rust/validation.md new file mode 100644 index 000000000..dff7d07e4 --- /dev/null +++ b/docs/rust/validation.md @@ -0,0 +1,135 @@ +# Validation + +Everything on this page runs after argv is bound and env/default fallbacks are applied. Only the +command that actually ran is judged. Contradictory declarations — `choices` on a `bool`, +`var_min` greater than `var_max`, a default that isn't one of the choices — are compile errors, +not runtime surprises. + +## Choices and bounds + +```rust +/// Output format +#[usage(long, choices("json", "table"))] +format: Option, + +/// Patterns to include +#[usage(long, var_min = 1, var_max = 5)] +include: Vec, +``` + +A value outside the set is `Error::InvalidChoice { name, choices }`; too few or too many values +are `VarTooFew`/`VarTooMany`. For enum-shaped values prefer +[`ValueEnum`](/rust/subcommands#value-enums). + +## Flag relations + +`conflicts`, `requires`, `overrides`, `required_if`, and `required_unless` relate one flag to +another. Targets are named the way the KDL spec names them (`"--long"` or `"-s"`), and naming a +flag that doesn't exist is a compile error: + +```rust +/// Read from standard input +#[usage(long, conflicts("--file", "--url"))] +stdin: bool, + +/// Retry count +#[usage(long, required_if("--retry"))] +max_retries: Option, +``` + +`overrides` is the quiet sibling of `conflicts`: a later occurrence of one flag discards an +earlier occurrence of the other instead of erroring — useful for `--json` / `--yaml` pairs where +the last one typed should win. + +## Groups + +A group relates several flags at once: membership is declared on each field, the group's +properties on the struct. + +```rust +#[derive(Cli)] +#[usage(bin = "grp")] +#[usage(group("input", required))] +struct Grp { + /// Read from a file + #[usage(long, group = "input")] + file: Option, + /// Read from a URL + #[usage(long, group = "input")] + url: Option, + /// Read from standard input + #[usage(short = 's', long, group = "input")] + stdin: bool, +} +``` + +The two properties compose the way clap's do: + +| Declaration | Meaning | +| ----------------------------------- | ------------ | +| `group("name")` | at most one | +| `group("name", required)` | exactly one | +| `group("name", required, multiple)` | at least one | + +An unsatisfied required group is `Error::MissingGroup { group, members }`, rendered as clap +renders it: + +``` +error: one of the following required arguments was not provided (input): + --file + --url + -s, --stdin +``` + +Two members of a non-`multiple` group produce `ConflictingFlags` — matched by flag, not +spelling, so giving one member as `-s` and another as `--file` still counts. A conflict is +reported before an unsatisfied group. + +Groups are emitted into the KDL spec +(`group "input" "--file" "--url" "--stdin" required=#true`), and a group declared on a +[flattened](/rust/subcommands#sharing-declarations-with-flatten) struct is enforced on every +command that flattens it. Malformed groups — one member, no members, declared twice, a group on +a positional — are compile errors. + +## Exclusive flags + +An `exclusive` flag has to be given alone — no other flag, no argument, no subcommand: + +```rust +/// Dump the spec and leave +#[usage(long, exclusive)] +dump: bool, +``` + +This is stronger than `conflicts` with every other flag, because `conflicts` has nowhere to name +an _argument_. The details: + +- `--dump -v` and `--dump somefile` both fail with `ConflictingFlags`. +- An exclusive flag **bypasses required-ness**: required siblings the flag's command declares are + not demanded when the exclusive flag is given — the `--version`-style escape hatch. +- Declared defaults still apply; only values the user actually _supplied_ count as company — + but a value supplied via `env` does count. +- Exclusivity crosses command boundaries in both directions: selecting a subcommand is company + for a parent's exclusive flag, and a parent's flags are company for a child's. + +`exclusive` on a positional is a compile error. Emitted KDL: `flag "--dump" exclusive=#true`. + +## Delimiters + +`delimiter` splits one word into several values, the way clap's `value_delimiter` does: + +```rust +/// Tags to apply +#[usage(long, delimiter = ',', var_max = 3)] +tags: Vec, +``` + +`--tags a,b,c` yields `["a", "b", "c"]`, and occurrences accumulate: `--tags a,b --tags c` is +`["a", "b", "c"]`. It works on positionals too (`#[usage(arg, delimiter = ';')]`). + +The split runs after `env` fallback and **before every check**, so `choices` judges each split +value and `var_min`/`var_max` count values, not words — `--tags a,b,c,d` with `var_max = 3` is +`VarTooMany { got: 4 }`. + +The field must be a `Vec`, and the delimiter must be a single ASCII character; both are enforced +at compile time. Emitted KDL: `flag "--tags " var=#true delimiter=","`. From d634d9b067cc4688e2ea999d3bc0c05a421f6bc8 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:55:47 +0000 Subject: [PATCH 2/2] docs: mark the framework docs as draft Every page in the new /rust/ and /go/ sections now opens with a draft disclaimer, since parts of what they document are still in open PRs. Co-Authored-By: Claude Fable 5 --- docs/go/binding.md | 5 +++++ docs/go/completions.md | 5 +++++ docs/go/generated-code.md | 5 +++++ docs/go/help.md | 5 +++++ docs/go/index.md | 6 ++++-- docs/go/parser.md | 5 +++++ docs/rust/args-and-flags.md | 5 +++++ docs/rust/completions.md | 5 +++++ docs/rust/help.md | 5 +++++ docs/rust/index.md | 5 +++-- docs/rust/spec.md | 5 +++++ docs/rust/subcommands.md | 5 +++++ docs/rust/validation.md | 5 +++++ 13 files changed, 62 insertions(+), 4 deletions(-) diff --git a/docs/go/binding.md b/docs/go/binding.md index 8faee11aa..4a228b660 100644 --- a/docs/go/binding.md +++ b/docs/go/binding.md @@ -1,5 +1,10 @@ # Binding and Values +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + Generated `Parse` does everything on this page for you. It's documented separately because the pieces are public — custom binding loops use them directly — and because the _rules_ matter even when you never call the functions: they define what your users' command lines mean. diff --git a/docs/go/completions.md b/docs/go/completions.md index 4327d933a..17ce2e76f 100644 --- a/docs/go/completions.md +++ b/docs/go/completions.md @@ -1,5 +1,10 @@ # Completions +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + The Go runtime answers the question every completion request boils down to — _what could go where the cursor is?_ — from the same tables the parser runs on, so completions can never disagree with the grammar. diff --git a/docs/go/generated-code.md b/docs/go/generated-code.md index c3609ede8..1a7fa26a4 100644 --- a/docs/go/generated-code.md +++ b/docs/go/generated-code.md @@ -1,5 +1,10 @@ # Generated Code +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + `usage generate go` lowers a KDL spec into one Go file. The output is `gofmt`-clean and carries the standard `// Code generated … DO NOT EDIT.` header. diff --git a/docs/go/help.md b/docs/go/help.md index 414a8927b..7e85f9834 100644 --- a/docs/go/help.md +++ b/docs/go/help.md @@ -1,5 +1,10 @@ # Help and Errors +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + ## Help pages Three renderers cover the usage line, the `-h` page, and the `--help` page: diff --git a/docs/go/index.md b/docs/go/index.md index dc08e7a20..dfd4736a0 100644 --- a/docs/go/index.md +++ b/docs/go/index.md @@ -1,8 +1,10 @@ # Go Framework -::: warning Experimental +::: warning Experimental — draft docs The Go framework is experimental. Its parsing behavior is verified against the same conformance -corpus as the Rust implementation, but APIs may still change between releases. +corpus as the Rust implementation, but APIs may still change between releases. These docs are a +draft: some of what they document is still in open pull requests, and details may change before +release. ::: The Go framework builds your CLI from a usage spec — but unlike most Go CLI libraries, your diff --git a/docs/go/parser.md b/docs/go/parser.md index 1bf922478..5e2362e38 100644 --- a/docs/go/parser.md +++ b/docs/go/parser.md @@ -1,5 +1,10 @@ # The Parser +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + Generated `Parse` is the front door, but the event-level API underneath is public and stable — use it when you need custom binding, a REPL, or completion positions. diff --git a/docs/rust/args-and-flags.md b/docs/rust/args-and-flags.md index 74d69720e..60cac2110 100644 --- a/docs/rust/args-and-flags.md +++ b/docs/rust/args-and-flags.md @@ -1,5 +1,10 @@ # Args and Flags +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + A field on a `#[derive(Cli)]` or `#[derive(Args)]` struct becomes a flag when it carries `long` or `short`, and a positional argument otherwise (or explicitly with `#[usage(arg)]`). diff --git a/docs/rust/completions.md b/docs/rust/completions.md index b44badc5d..50c6a6ae9 100644 --- a/docs/rust/completions.md +++ b/docs/rust/completions.md @@ -1,5 +1,10 @@ # Completions +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + Completion support is opt-in: add `completion` to the root attribute and enable the `completions` cargo feature (forgetting the feature is a compile error that names it): diff --git a/docs/rust/help.md b/docs/rust/help.md index caa413bfb..1ccfbe5e1 100644 --- a/docs/rust/help.md +++ b/docs/rust/help.md @@ -1,5 +1,10 @@ # Help, Version, and Errors +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + ## Help `-h` and `--help` are supplied by the parser — you never declare them. They aren't written into diff --git a/docs/rust/index.md b/docs/rust/index.md index f06468b15..636071a61 100644 --- a/docs/rust/index.md +++ b/docs/rust/index.md @@ -1,8 +1,9 @@ # Rust Framework -::: warning Experimental +::: warning Experimental — draft docs The Rust framework is experimental. It is complete enough that `usage-cli` itself is built with it, -but attribute names and APIs may still change between releases. +but attribute names and APIs may still change between releases. These docs are a draft: some of +what they document is still in open pull requests, and details may change before release. ::: The Rust framework builds your CLI from Rust types. You declare commands, flags, and args as diff --git a/docs/rust/spec.md b/docs/rust/spec.md index cc64ef5ee..84b9c770b 100644 --- a/docs/rust/spec.md +++ b/docs/rust/spec.md @@ -1,5 +1,10 @@ # Spec Output +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + `Cli::to_kdl()` writes a complete [usage spec](/spec/) from the same static metadata the parser runs on. This is the bridge to the rest of the toolkit: markdown docs, manpages, completion scripts for other consumers, SDK generation, and linting all consume that KDL. diff --git a/docs/rust/subcommands.md b/docs/rust/subcommands.md index c1ac2c9a9..d4a8bdccd 100644 --- a/docs/rust/subcommands.md +++ b/docs/rust/subcommands.md @@ -1,5 +1,10 @@ # Subcommands +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + Subcommands are an enum. Each variant wraps a `#[derive(Args)]` struct (or nothing), and the enum derives `Subcommands`: diff --git a/docs/rust/validation.md b/docs/rust/validation.md index dff7d07e4..5baf564ab 100644 --- a/docs/rust/validation.md +++ b/docs/rust/validation.md @@ -1,5 +1,10 @@ # Validation +::: warning Draft +This page is a draft. Some of what it documents is still in open pull requests, and details may +change before release. +::: + Everything on this page runs after argv is bound and env/default fallbacks are applied. Only the command that actually ran is judged. Contradictory declarations — `choices` on a `bool`, `var_min` greater than `var_max`, a default that isn't one of the choices — are compile errors,