Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
18 changes: 7 additions & 11 deletions docs/.vitepress/theme/UsageHero.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,33 @@ const javascriptPath =
<p class="usage-hero-desc">
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,
<code>--help</code>, docs, and manpages from that one definition. Reference
<code>--help</code>, 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.
</p>

<p class="usage-hero-label">Frameworks</p>
<div class="usage-hero-tiles">
<a
href="https://github.com/jdx/usage/tree/main/derive"
class="usage-tile usage-tile-live"
>
<a href="/rust/" class="usage-tile usage-tile-live">
<svg class="usage-tile-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path :d="rustPath" />
</svg>
<span>Rust</span>
<span class="usage-tile-experimental-pill">experimental</span>
<div class="usage-tile-tooltip">
<strong>Rust framework</strong>
<p>Derive your CLI from Rust types — parsing, help, and completions generated from the usage spec.</p>
<p>Derive your CLI from Rust types — parsing, help, and completions generated from the usage spec. Experimental: APIs may change between releases.</p>
</div>
</a>
<a
href="https://github.com/jdx/usage/tree/main/go"
class="usage-tile usage-tile-live"
>
<a href="/go/" class="usage-tile usage-tile-live">
<svg class="usage-tile-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path :d="goPath" />
</svg>
<span>Go</span>
<span class="usage-tile-experimental-pill">experimental</span>
<div class="usage-tile-tooltip">
<strong>Go framework</strong>
<p>Build Go CLIs on the usage spec, with parsing behavior verified against the same conformance corpus as the Rust implementation.</p>
<p>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.</p>
</div>
</a>
<span class="usage-tile usage-tile-soon">
Expand Down
8 changes: 7 additions & 1 deletion docs/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
83 changes: 83 additions & 0 deletions docs/go/binding.md
Original file line number Diff line number Diff line change
@@ -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::<i64>()` 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.
84 changes: 84 additions & 0 deletions docs/go/completions.md
Original file line number Diff line number Diff line change
@@ -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

```
<bin> __complete_word__ --shell <shell> --line "<text before cursor>"
```

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).
120 changes: 120 additions & 0 deletions docs/go/generated-code.md
Original file line number Diff line number Diff line change
@@ -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 <FILE>` | the KDL spec (`-` for stdin) |
| `--spec <SPEC>` | a raw spec string instead of a file |
| `-o --out-file <PATH>` | output path (`-` for stdout) |
| `-p --package <NAME>` | 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
```
Loading