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..4a228b660 --- /dev/null +++ b/docs/go/binding.md @@ -0,0 +1,83 @@ +# 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. + +## 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..17ce2e76f --- /dev/null +++ b/docs/go/completions.md @@ -0,0 +1,84 @@ +# 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. + +## 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..1a7fa26a4 --- /dev/null +++ b/docs/go/generated-code.md @@ -0,0 +1,120 @@ +# 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. + +```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..7e85f9834 --- /dev/null +++ b/docs/go/help.md @@ -0,0 +1,87 @@ +# 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: + +```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..dfd4736a0 --- /dev/null +++ b/docs/go/index.md @@ -0,0 +1,141 @@ +# Go Framework + +::: 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. 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 +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..5e2362e38 --- /dev/null +++ b/docs/go/parser.md @@ -0,0 +1,80 @@ +# 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. + +```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..60cac2110 --- /dev/null +++ b/docs/rust/args-and-flags.md @@ -0,0 +1,159 @@ +# 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)]`). + +```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..50c6a6ae9 --- /dev/null +++ b/docs/rust/completions.md @@ -0,0 +1,99 @@ +# 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): + +```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..1ccfbe5e1 --- /dev/null +++ b/docs/rust/help.md @@ -0,0 +1,82 @@ +# 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 +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..636071a61 --- /dev/null +++ b/docs/rust/index.md @@ -0,0 +1,156 @@ +# Rust Framework + +::: 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. 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 +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..84b9c770b --- /dev/null +++ b/docs/rust/spec.md @@ -0,0 +1,99 @@ +# 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. + +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..d4a8bdccd --- /dev/null +++ b/docs/rust/subcommands.md @@ -0,0 +1,157 @@ +# 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`: + +```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..5baf564ab --- /dev/null +++ b/docs/rust/validation.md @@ -0,0 +1,140 @@ +# 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, +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=","`.