feat(argv): add a zero-allocation argv parser - #798
Conversation
📝 WalkthroughWalkthroughThe PR adds the Changesusage-argv parser
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Corpus as conformance corpus
participant Runner as conformance::argv::run
participant Parser
Corpus->>Runner: provide Vector
Runner->>Parser: construct parser tables and parse argv
Parser-->>Runner: emit Event or Error
Runner-->>Corpus: return Outcome
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Instruction counts
No instruction-count regression above 1%. Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run. Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.
|
Greptile SummaryThe PR adds the zero-allocation
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported short-bundle defects are fixed in the current code. Important Files Changed
Reviews (3): Last reviewed commit: "chore(argv): release usage-argv on the s..." | Re-trigger Greptile |
usage-argv implements the binding half of the argv grammar: which token becomes which flag or argument, when a word routes to a subcommand, and what is an error. It builds no command tree, allocates nothing on any outcome, and reads argv once. The tables are borrowed slices so a derive can emit them as static data, and parsing yields events rather than a map — generated code can assign an event straight into a struct field, which is both faster and the reason no allocation is needed. Values come back as bytes borrowed from argv, so a non-UTF-8 command line still parses and only the values actually inspected can fail to convert. Scope is binding only. required, choices, env, defaults, var_min/var_max and overrides are decided after the last token and need a value's type, so they belong to the layer that owns the target struct. Keeping them out is what makes the loop small. Verified two ways: 61 of the corpus's 83 vectors run through this parser (the other 22 are post-binding, and that count is asserted), and a counting global allocator proves both successful and failing parses allocate zero times. All 15 vectors where usage-lib diverges from the grammar are among the ones this parser gets right. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corpus gained a vector for `double_dash="automatic"`, which is a binding rule — it decides whether a later token is a flag or a value — so it belongs in this parser rather than in the layer above. Once an argument in that mode takes a value, flag interpretation stops as though the caller had typed the separator. usage-argv now answers 62 of the corpus's 86 vectors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both review bots caught the same pair of bugs, independently. `-fz` emitted the `-f` event and only then discovered that `z` matches nothing, so a rejected command line left an earlier flag applied. Events go out one at a time, so the fix is to walk the whole token first and refuse it before binding anything; the scan stops at the first value-taking letter, since everything after that is its value. The error also carried an empty slice: `self.bundle` was cleared before the token was read out of it. The whole token as typed is now kept for the length of the bundle, so the error names `-fz` — which is also the unit in which it is rejected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publishes usage-argv as an ordinary member of the workspace at the shared version rather than holding it at 0.0.0 outside the release cycle. The release script needed four adjustments to make that correct rather than half-true: publish the crate (and catch it up the way clap_usage is), include argv/ when computing the version bump and the changelog so an argv-only change can cut a release, stage its bumped manifest — the commit listed only root, cli, and lib, so the bump would have been dropped — and exclude usage-conformance from the bump, since the harness is never published and does not need a version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 180aec7. Configure here.
| // the bare name. | ||
| negate: f.negate.as_ref().map(|n| leak(n.trim_start_matches('-'))), | ||
| takes_value: f.arg.is_some(), | ||
| var: f.var || f.arg.as_ref().is_some_and(|a| a.var), |
There was a problem hiding this comment.
Repeatable flag triggers greedy collection
Medium Severity
When building Flag tables, var is set from both spec var=#true and a variadic flag argument (...), but the parser treats Flag.var as greedy multi-value collection for a single flag occurrence. A repeatable flag like --include <pattern> with var=#true should take one value per occurrence; tokens after the first value should go to positionals, not keep filling the same flag.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 180aec7. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
argv/src/lib.rs (1)
624-627: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd unit coverage for variadic flags and for
Error::TooDeep.No test in this module declares a flag with
var: true, so thecollectingpath instep(Lines 344-356) and the two assignment sites (Lines 421 and 492) are unit-tested only through the conformance crate. No test reachesMAX_DEPTHeither, soError::TooDeepis unexercised. Both paths carry the divergence flagged at Lines 421-423.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@argv/src/lib.rs` around lines 624 - 627, Add unit tests in the existing tests module covering flags configured with var: true, exercising the collecting path in step and both assignment sites, including the expected variadic parsing behavior. Add a separate test that constructs nesting beyond MAX_DEPTH and asserts Error::TooDeep, preserving existing test conventions and validating the divergence-prone paths directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@argv/src/lib.rs`:
- Around line 421-423: Update the long-flag handling around the collecting
assignment so collecting is set only when the flag’s takes_value property is
true, matching the short_flag path. Ensure var-only flags that do not take
values leave collecting unset and do not consume following words or emit a
value.
- Around line 307-310: Correct the state exposed by Args::double_dash_seen so it
reports true only when the user explicitly supplied a -- separator, rather than
when word applies DoubleDash::Automatic and sets self.double_dash. Track
explicit consumption separately from automatic flag interpretation, and have the
accessor return that explicit-separator state while preserving existing parsing
behavior.
- Around line 1069-1079: Update non_utf8_values_still_parse to construct a
command-line argument containing actual invalid UTF-8 bytes as a value, rather
than using the valid "--force" flag. Assert that parse accepts and binds this
non-UTF-8 value, while retaining the as_str rejection assertion for invalid
bytes.
In `@conformance/src/argv.rs`:
- Around line 57-59: Update the out_of_scope check around out_of_scope so
post-binding declarations are evaluated only after token routing identifies the
selected command path, including applicable inherited flags. Avoid scanning
declarations from unselected subcommands, while preserving the existing
Outcome::OutOfScope behavior for declarations that apply to the selected path.
In `@conformance/tests/argv.rs`:
- Around line 28-53: Update the corpus loop in conformance/tests/argv.rs around
corpus() and Outcome::OutOfScope to count out-of-scope results and directly
assert that the count is 24, while preserving the existing in-scope and failure
assertions. In docs/spec/argv.md lines 248-253, keep the documented
excluded-vector count aligned with this 24-vector assertion.
---
Nitpick comments:
In `@argv/src/lib.rs`:
- Around line 624-627: Add unit tests in the existing tests module covering
flags configured with var: true, exercising the collecting path in step and both
assignment sites, including the expected variadic parsing behavior. Add a
separate test that constructs nesting beyond MAX_DEPTH and asserts
Error::TooDeep, preserving existing test conventions and validating the
divergence-prone paths directly.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 83d2c706-4154-40d9-9ee6-26544fe0b6c2
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
Cargo.tomlargv/Cargo.tomlargv/src/lib.rsargv/tests/no_alloc.rsconformance/Cargo.tomlconformance/src/argv.rsconformance/src/lib.rsconformance/tests/argv.rsdocs/spec/argv.mdtasks/release-plz
| /// Whether a `--` has been consumed as a separator. | ||
| pub fn double_dash_seen(&self) -> bool { | ||
| self.double_dash | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
double_dash_seen reports true when no -- was given.
word sets self.double_dash for DoubleDash::Automatic (Line 540). The accessor then returns true although the user typed no separator, which contradicts the documented meaning. A caller that reproduces the command line, or that reports how a value was accepted, gets the wrong answer.
Track the two states apart, or document the accessor as "flag interpretation has stopped".
♻️ Proposed direction
- /// Whether a `--` has been consumed as a separator.
+ /// Whether flag interpretation has stopped, either because a `--` was
+ /// consumed or because a `double_dash = "automatic"` argument took a value.
pub fn double_dash_seen(&self) -> bool {
self.double_dash
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Whether a `--` has been consumed as a separator. | |
| pub fn double_dash_seen(&self) -> bool { | |
| self.double_dash | |
| } | |
| /// Whether flag interpretation has stopped, either because a `--` was | |
| /// consumed or because a `double_dash = "automatic"` argument took a value. | |
| pub fn double_dash_seen(&self) -> bool { | |
| self.double_dash | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@argv/src/lib.rs` around lines 307 - 310, Correct the state exposed by
Args::double_dash_seen so it reports true only when the user explicitly supplied
a -- separator, rather than when word applies DoubleDash::Automatic and sets
self.double_dash. Track explicit consumption separately from automatic flag
interpretation, and have the accessor return that explicit-separator state while
preserving existing parsing behavior.
| if flag.var { | ||
| self.collecting = Some(flag); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Set collecting only when the flag takes a value.
long_flag sets collecting for any var flag, but short_flag (Line 492) sets it only inside the takes_value branch. A flag declared with var: true and takes_value: false therefore binds differently through its long form than through its short form: the long form swallows the following words and emits Event::Flag { value: Some(..) } for a flag that declares no value.
Align the two paths.
🐛 Proposed fix
- if flag.var {
+ if flag.takes_value && flag.var {
self.collecting = Some(flag);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if flag.var { | |
| self.collecting = Some(flag); | |
| } | |
| if flag.takes_value && flag.var { | |
| self.collecting = Some(flag); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@argv/src/lib.rs` around lines 421 - 423, Update the long-flag handling around
the collecting assignment so collecting is set only when the flag’s takes_value
property is true, matching the short_flag path. Ensure var-only flags that do
not take values leave collecting unset and do not consume following words or
emit a value.
| #[test] | ||
| fn non_utf8_values_still_parse() { | ||
| // A value that is not valid UTF-8 binds; only converting it fails, and | ||
| // only if a caller asks. | ||
| let raw = OsStr::new("--force"); | ||
| let a = [raw]; | ||
| assert!(parse(&ROOT, &a).is_ok()); | ||
|
|
||
| assert!(as_str(b"ok").is_ok()); | ||
| assert!(as_str(&[0xff, 0xfe]).is_err()); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
The test does not use a non-UTF-8 command line.
raw is "--force", which is valid UTF-8 and binds as a flag, not as a value. The assertion at Line 1075 passes regardless of the property the test names. Feed real non-UTF-8 bytes and assert that the value binds while as_str rejects it.
💚 Proposed fix
#[test]
fn non_utf8_values_still_parse() {
// A value that is not valid UTF-8 binds; only converting it fails, and
// only if a caller asks.
- let raw = OsStr::new("--force");
- let a = [raw];
- assert!(parse(&ROOT, &a).is_ok());
+ #[cfg(unix)]
+ {
+ use std::os::unix::ffi::OsStrExt;
+ let raw = OsStr::from_bytes(&[b'a', 0xff, b'z']);
+ let a = [raw];
+ let events = parse(&ROOT, &a).unwrap();
+ let Event::Arg { value, .. } = events[0] else {
+ panic!("expected an arg");
+ };
+ assert_eq!(value, &[b'a', 0xff, b'z'][..]);
+ assert!(as_str(value).is_err());
+ }
assert!(as_str(b"ok").is_ok());
assert!(as_str(&[0xff, 0xfe]).is_err());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[test] | |
| fn non_utf8_values_still_parse() { | |
| // A value that is not valid UTF-8 binds; only converting it fails, and | |
| // only if a caller asks. | |
| let raw = OsStr::new("--force"); | |
| let a = [raw]; | |
| assert!(parse(&ROOT, &a).is_ok()); | |
| assert!(as_str(b"ok").is_ok()); | |
| assert!(as_str(&[0xff, 0xfe]).is_err()); | |
| } | |
| #[test] | |
| fn non_utf8_values_still_parse() { | |
| // A value that is not valid UTF-8 binds; only converting it fails, and | |
| // only if a caller asks. | |
| #[cfg(unix)] | |
| { | |
| use std::os::unix::ffi::OsStrExt; | |
| let raw = OsStr::from_bytes(&[b'a', 0xff, b'z']); | |
| let a = [raw]; | |
| let events = parse(&ROOT, &a).unwrap(); | |
| let Event::Arg { value, .. } = events[0] else { | |
| panic!("expected an arg"); | |
| }; | |
| assert_eq!(value, &[b'a', 0xff, b'z'][..]); | |
| assert!(as_str(value).is_err()); | |
| } | |
| assert!(as_str(b"ok").is_ok()); | |
| assert!(as_str(&[0xff, 0xfe]).is_err()); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@argv/src/lib.rs` around lines 1069 - 1079, Update non_utf8_values_still_parse
to construct a command-line argument containing actual invalid UTF-8 bytes as a
value, rather than using the valid "--force" flag. Assert that parse accepts and
binds this non-UTF-8 value, while retaining the as_str rejection assertion for
invalid bytes.
| if let Some(reason) = out_of_scope(&spec, &vector.expect) { | ||
| return Outcome::OutOfScope(reason); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Limit out-of-scope checks to the selected command path.
out_of_scope runs before token routing. declares_post_binding then scans every subcommand. A default, choice, or requirement in an unselected subcommand causes this runner to skip a vector that usage-argv can bind.
Evaluate post-binding declarations only for the selected command path and applicable inherited flags. This prevents valid binding vectors from being excluded and hiding parser regressions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@conformance/src/argv.rs` around lines 57 - 59, Update the out_of_scope check
around out_of_scope so post-binding declarations are evaluated only after token
routing identifies the selected command path, including applicable inherited
flags. Avoid scanning declarations from unselected subcommands, while preserving
the existing Outcome::OutOfScope behavior for declarations that apply to the
selected path.
| for vector in corpus() { | ||
| let outcome = run(&vector); | ||
| if let Outcome::OutOfScope(_) = outcome { | ||
| continue; | ||
| } | ||
| in_scope += 1; | ||
| if !outcome.matches(&vector.expect) { | ||
| failures.push(format!( | ||
| "{}: {}\n expected: {:?}\n got: {outcome:?}", | ||
| vector.id, vector.doc, vector.expect | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| assert!( | ||
| failures.is_empty(), | ||
| "{} binding vector(s) failed:\n - {}", | ||
| failures.len(), | ||
| failures.join("\n - ") | ||
| ); | ||
|
|
||
| assert_eq!( | ||
| in_scope, IN_SCOPE, | ||
| "the number of vectors usage-argv answers changed; if that was the point \ | ||
| of your change, update IN_SCOPE" | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the out-of-scope count directly.
The test only asserts that 62 vectors are in scope. If the corpus adds an out-of-scope vector, this assertion can still pass. This does not enforce the documented 24-vector exclusion set.
conformance/tests/argv.rs#L28-L53: countOutcome::OutOfScoperesults and assert that the count is 24.docs/spec/argv.md#L248-L253: keep the documented count aligned with the direct test assertion.
Proposed test change
const IN_SCOPE: usize = 62;
+const OUT_OF_SCOPE: usize = 24;
let mut failures = Vec::new();
let mut in_scope = 0;
+ let mut out_of_scope = 0;
- if let Outcome::OutOfScope(_) = outcome {
+ if let Outcome::OutOfScope(_) = outcome {
+ out_of_scope += 1;
continue;
}
+ assert_eq!(out_of_scope, OUT_OF_SCOPE);
assert_eq!(in_scope, IN_SCOPE);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for vector in corpus() { | |
| let outcome = run(&vector); | |
| if let Outcome::OutOfScope(_) = outcome { | |
| continue; | |
| } | |
| in_scope += 1; | |
| if !outcome.matches(&vector.expect) { | |
| failures.push(format!( | |
| "{}: {}\n expected: {:?}\n got: {outcome:?}", | |
| vector.id, vector.doc, vector.expect | |
| )); | |
| } | |
| } | |
| assert!( | |
| failures.is_empty(), | |
| "{} binding vector(s) failed:\n - {}", | |
| failures.len(), | |
| failures.join("\n - ") | |
| ); | |
| assert_eq!( | |
| in_scope, IN_SCOPE, | |
| "the number of vectors usage-argv answers changed; if that was the point \ | |
| of your change, update IN_SCOPE" | |
| ); | |
| let mut failures = Vec::new(); | |
| let mut in_scope = 0; | |
| let mut out_of_scope = 0; | |
| for vector in corpus() { | |
| let outcome = run(&vector); | |
| if let Outcome::OutOfScope(_) = outcome { | |
| out_of_scope += 1; | |
| continue; | |
| } | |
| in_scope += 1; | |
| if !outcome.matches(&vector.expect) { | |
| failures.push(format!( | |
| "{}: {}\n expected: {:?}\n got: {outcome:?}", | |
| vector.id, vector.doc, vector.expect | |
| )); | |
| } | |
| } | |
| assert!( | |
| failures.is_empty(), | |
| "{} binding vector(s) failed:\n - {}", | |
| failures.len(), | |
| failures.join("\n - ") | |
| ); | |
| assert_eq!(out_of_scope, OUT_OF_SCOPE); | |
| assert_eq!( | |
| in_scope, IN_SCOPE, | |
| "the number of vectors usage-argv answers changed; if that was the point \ | |
| of your change, update IN_SCOPE" | |
| ); |
📍 Affects 2 files
conformance/tests/argv.rs#L28-L53(this comment)docs/spec/argv.md#L248-L253
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@conformance/tests/argv.rs` around lines 28 - 53, Update the corpus loop in
conformance/tests/argv.rs around corpus() and Outcome::OutOfScope to count
out-of-scope results and directly assert that the count is 24, while preserving
the existing in-scope and failure assertions. In docs/spec/argv.md lines
248-253, keep the documented excluded-vector count aligned with this 24-vector
assertion.
Two review findings from #798. A flag declared `var=#true` with a single-value argument is repeatable — one value per occurrence — while a flag with a variadic argument (`<pattern>...`) is greedy. The conformance harness set the parser's flag from either, so `--include a b` gave a merely repeatable flag both values and silently stole the positional that `b` should have filled. The grammar already drew this distinction; nothing tested it, so there is now a vector that does, and usage-lib agrees with it. The field invited the mistake by sharing a name with the spec's flag-level `var`, which means something else, so it is now `variadic` and says what it is not. Renaming it is free today because no release has published the crate yet. `double_dash_seen()` also reported true when no separator had been typed: automatic mode stops flag interpretation by setting the same flag the accessor reads. Those are now two pieces of state, since a caller asking the question wants to know what the user wrote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two findings from the review of #798, both real. ## A repeatable flag was greedy The spec has two similar-looking declarations that mean different things: - `flag "--include <pattern>" var=#true` — **repeatable**: one value per occurrence - `flag "--include <pattern>..."` — **variadic**: one occurrence takes several values The conformance harness set the parser's greedy flag from either, so `--include a b` gave a merely repeatable flag both values — and silently stole the positional that `b` should have filled. The grammar already drew the distinction; nothing tested it. There is now a vector that does, and usage-lib agrees with it, so this was ours alone. The field name invited the mistake — `Flag.var` sat next to the spec's flag-level `var`, which means the other thing — so it is now `Flag.variadic` with a doc comment saying what it is *not*. Free to rename today, since no release has published the crate. ## `double_dash_seen()` could lie `automatic` mode stops flag interpretation by setting the same field the accessor reads, so it reported a separator that the user never typed. Now two pieces of state: `flags_stopped` for the parser, `separator_seen` for the question callers actually ask. usage-lib draws the same line for `preserve`, where a `--` is kept as a value rather than consumed. Three unit tests cover the pair, including the counterpart case — a non-variadic flag must leave the next word alone. 87 vectors, 63 answered by usage-argv, 16 recorded usage-lib divergences. *AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.* <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes argv binding semantics for specs that relied on the old greedy `var` mapping; API rename on unreleased `Flag` field and behavior change for `double_dash_seen()` in edge cases. > > **Overview** > Fixes **usage-argv** conflating spec **repeatable** flags (`var=#true`, one value per occurrence) with **variadic** flag arguments (`<pattern>...`, greedy until a flag-like token). The conformance bridge no longer sets parser greed from flag-level `var`; only a variadic argument enables value collection. **`Flag.var` is renamed to `variadic`** with docs clarifying it is not flag-level `var`. > > **`double_dash_seen()`** is corrected by splitting parser state: `flags_stopped` (flag interpretation off, including `automatic` args) vs `separator_seen` (a real `--` was consumed). Required-arg checks use `separator_seen` so `preserve` / `automatic` do not lie to callers. > > Adds corpus vector **`long-repeatable-flag-is-not-greedy`**, unit tests, and doc/corpus count updates (63 in-scope vectors). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8ef1ff2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Repeatable flags now consume exactly one value per occurrence, leaving subsequent values available to positional arguments. * Variadic flags correctly consume multiple values from a single occurrence. * Improved handling and reporting of explicit `--` separators, including positional arguments and subcommands. * **Documentation** * Clarified flag value consumption rules and updated command-line conformance statistics. * Added coverage for repeatable, variadic, and separator-handling scenarios. * **Refactor** * Renamed the public flag property from `var` to `variadic` for clearer behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`PLAN.md` — the plan for this work written down end to end, including the parts that do not exist yet. Three PRs in (#797, #798, #799), the plan lived in PR descriptions and in my head. That is fine for one PR and not for a dozen, especially for the config layer, where the shape is worth arguing about *before* it gets built. Checkboxes rather than prose, so the file doubles as status: **an unchecked box means the thing does not exist.** Ticking them as things land keeps it honest, and makes it obvious when a branch of the plan has stalled. ## What it covers - **Why** — mise's measured numbers, and the fact that mise already hand-maintains two argv scanners to avoid building its clap tree. That workaround existing is the argument for the project. - **How it is arranged** — the four rules that hold it together: code authors and the spec defines; usage-lib is the reference implementation; the hot path stays small; end users never need a second binary. - **Milestones** — what is done, the derive work next, the table stakes after it (help, self-contained completions, docs, diagnostics). - **The gate** — the perf targets, measured with `tak` against a shadow CLI generated from mise's own committed spec. Explicitly: if the targets miss by a wide margin, write that down and stop. Nothing touches mise before this. - **Known usage-lib divergences** — as a to-do list, since each is a small change to `lib/src/parse.rs` and the corpus already knows how to verify a fix. - **Config** — the v2 design, from reading all four CLIs. ## The config section is the part worth reviewing mise, hk, pitchfork, and fnox have each independently built the same settings model — a TOML registry, `build.rs` codegen, a typed `Settings` plus a meta map, project-over-global-over-defaults layering — and agree on ~80% of the vocabulary. The differences are mostly *drift* rather than intent: - Every one hand-writes the CLI-to-settings binding, and every one has a hole in it: hk declares `sources.cli` entries nothing reads, pitchfork's `--help` documents a CLI layer it does not have (copied by hand into its committed spec), and fnox resolves `age_key_file` through a hardcoded five-way chain because its settings and config files are separate systems. - Only hk can say where a value came from, and it needed a second parallel merge to do it. - Docs/schema generation is three separate reimplementations, and fnox has none. The proposal is to declare props in code, lower them into the spec's `config { prop ... }` block — which **exists today and no CLI emits or consumes** — and generate the CLI binding instead of hand-writing it. That block needs extending first (`deprecated`, `enum`, `optional`, `aliases`, `merge`, scope, per-source lists), which is spec-first per the canonicality rule. Three open questions are listed rather than decided, including whether config belongs in this repo at all. *AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.* <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only addition with no runtime, build, or API changes. > > **Overview** > Introduces **`PLAN.md`** as the canonical, in-repo plan for the compiled argv parser work and the later shared config layer—replacing plan text that lived only in PR descriptions. > > The doc uses **unchecked checkboxes as status** (unchecked = not built yet) and covers motivation (mise/clap cost), architecture (spec → usage-derive / usage-argv / usage-lib), milestones (done vs derive vs gate vs adoption), perf gate targets, corpus gaps, known **usage-lib** divergences as a fix list, and a **config** design sketch (unify mise/hk/pitchfork/fnox settings) with open questions—not implementation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 96b60f9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
⚠️ **CAUTION: this is a major update, indicating a breaking change!**⚠️ This MR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [usage](https://github.com/jdx/usage) | tools | major | `5.1.0` → `6.2.0` | MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot). **Proposed changes to behavior should be submitted there as MRs.** --- ### Release Notes <details> <summary>jdx/usage (usage)</summary> ### [`v6.2.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#620---2026-08-24) [Compare Source](jdx/usage@v6.1.1...v6.2.0) ##### 🚀 Features - **(argv)** add embedded parse outcomes by [@​jdx](https://github.com/jdx) in [#​1250](jdx/usage#1250) - **(cli)** render inline formatting in help text by [@​jdx](https://github.com/jdx) in [#​1245](jdx/usage#1245) - **(cli)** split grouped help template sections by [@​jdx](https://github.com/jdx) in [#​1251](jdx/usage#1251) - **(complete)** add presentation labels to candidates by [@​jdx](https://github.com/jdx) in [#​1239](jdx/usage#1239) - **(complete)** expose structured completion traces by [@​jdx](https://github.com/jdx) in [#​1241](jdx/usage#1241) - **(complete)** add semantic candidate kinds by [@​jdx](https://github.com/jdx) in [#​1242](jdx/usage#1242) - **(complete)** add Elvish runtime completions by [@​jdx](https://github.com/jdx) in [#​1243](jdx/usage#1243) - **(derive)** let argument groups carry values by [@​jdx](https://github.com/jdx) in [#​1253](jdx/usage#1253) - **(derive)** add typed command finalization by [@​jdx](https://github.com/jdx) in [#​1254](jdx/usage#1254) - **(derive)** add runtime-computed defaults by [@​jdx](https://github.com/jdx) in [#​1256](jdx/usage#1256) - **(derive)** dispatch embedded control requests by [@​jdx](https://github.com/jdx) in [#​1270](jdx/usage#1270) - **(derive)** emit embedded\_outcome\_into for converted CLIs by [@​jdx](https://github.com/jdx) in [#​1281](jdx/usage#1281) - **(docs)** allow overriding markdown templates by [@​jdx](https://github.com/jdx) in [#​1267](jdx/usage#1267) - **(docs)** default to compact markdown references by [@​jdx](https://github.com/jdx) in [#​1272](jdx/usage#1272) - **(docs)** polish compact markdown references by [@​jdx](https://github.com/jdx) in [#​1280](jdx/usage#1280) - **(help)** expose addressable help topics by [@​jdx](https://github.com/jdx) in [#​1257](jdx/usage#1257) - **(help)** list commands by name in one aligned column by [@​jdx](https://github.com/jdx) in [#​1284](jdx/usage#1284) - **(help)** wrap the short help page by [@​jdx](https://github.com/jdx) in [#​1287](jdx/usage#1287) - **(parse)** add structured diagnostic reports by [@​jdx](https://github.com/jdx) in [#​1255](jdx/usage#1255) - **(parse)** add opt-in response files by [@​jdx](https://github.com/jdx) in [#​1259](jdx/usage#1259) - **(parse)** preserve ordered argument groups by [@​jdx](https://github.com/jdx) in [#​1271](jdx/usage#1271) - **(spec)** declare command outputs and exit codes by [@​jdx](https://github.com/jdx) in [#​1249](jdx/usage#1249) - **(spec)** add surface availability metadata by [@​jdx](https://github.com/jdx) in [#​1258](jdx/usage#1258) - **(spec)** add semantic note and warning blocks by [@​jdx](https://github.com/jdx) in [#​1273](jdx/usage#1273) - **(spec)** add output media types by [@​jdx](https://github.com/jdx) in [#​1274](jdx/usage#1274) - **(spec)** add help prose to heading sections by [@​jdx](https://github.com/jdx) in [#​1282](jdx/usage#1282) - add dynamic command catalogs by [@​jdx](https://github.com/jdx) in [#​1275](jdx/usage#1275) ##### 🐛 Bug Fixes - **(completion)** handle attached values and emit built-ins by [@​jdx](https://github.com/jdx) in [#​1277](jdx/usage#1277) - **(derive)** preserve flattened command metadata by [@​jdx](https://github.com/jdx) in [#​1268](jdx/usage#1268) - **(derive)** skip choice checks for typed defaults by [@​jdx](https://github.com/jdx) in [#​1269](jdx/usage#1269) - **(derive)** suppress generated partial field lint by [@​jdx](https://github.com/jdx) in [#​1278](jdx/usage#1278) - **(derive)** keep an invalid choice after an override displaces the flag by [@​jdx](https://github.com/jdx) in [#​1286](jdx/usage#1286) - **(spec)** make the two KDL writers agree on three more nodes by [@​jdx](https://github.com/jdx) in [#​1289](jdx/usage#1289) ##### 🚜 Refactor - **(deps)** replace versions with semver by [@​jdx](https://github.com/jdx) in [#​1285](jdx/usage#1285) ##### ⚡ Performance - **(argv)** reduce sort code size by [@​jdx](https://github.com/jdx) in [#​1264](jdx/usage#1264) - **(markdown)** skip empty admonition context by [@​jdx](https://github.com/jdx) in [#​1279](jdx/usage#1279) - document usage-rs parser tradeoffs by [@​jdx](https://github.com/jdx) in [#​1265](jdx/usage#1265) ##### 🛡️ Security - **(complete)** filter path candidates by extension by [@​jdx](https://github.com/jdx) in [#​1240](jdx/usage#1240) ##### 🔍 Other Changes - update usage of deprecated `str downcase` thingy in nushell by [@​TheBearodactyl](https://github.com/TheBearodactyl) in [#​1262](jdx/usage#1262) ##### New Contributors - [@​TheBearodactyl](https://github.com/TheBearodactyl) made their first contribution in [#​1262](jdx/usage#1262) ### [`v6.1.1`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#611---2026-08-23) [Compare Source](jdx/usage@v6.1.0...v6.1.1) ##### 🐛 Bug Fixes - **(argv)** simplify generated completion headers by [@​jdx](https://github.com/jdx) in [#​1226](jdx/usage#1226) - **(argv)** plan for the target platform, not the host by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​1233](jdx/usage#1233) - **(complete)** keep the path separator the caller typed by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​1230](jdx/usage#1230) - **(config)** report config paths without the verbatim prefix by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​1232](jdx/usage#1232) - **(docs)** separate visible flag aliases by [@​jdx](https://github.com/jdx) in [#​1228](jdx/usage#1228) - **(test)** compile the platform-conditional fixtures warning-free on windows by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​1234](jdx/usage#1234) ##### ⚡ Performance - **(derive)** outline invalid-value error construction from generated builds by [@​jdx](https://github.com/jdx) in [#​1235](jdx/usage#1235) - **(derive)** share the repeated-value collection loop across fields by [@​jdx](https://github.com/jdx) in [#​1236](jdx/usage#1236) ##### 🧪 Testing - **(windows)** let the suite run where zsh, fish and bash-completion are not by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​1229](jdx/usage#1229) ### [`v6.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#610---2026-08-22) [Compare Source](jdx/usage@v6.0.0...v6.1.0) ##### 🚀 Features - **(cli)** read settings under a prefix mise does not strip by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​1213](jdx/usage#1213) - **(derive)** dispatch more of the matches CLIs already write by [@​jdx](https://github.com/jdx) in [#​1221](jdx/usage#1221) - **(spec)** apply runtime identity and flatten headings in help by [@​jdx](https://github.com/jdx) in [#​1220](jdx/usage#1220) ##### 🐛 Bug Fixes - **(derive)** flow long help and emit kdl raw multiline strings by [@​jdx](https://github.com/jdx) in [#​1215](jdx/usage#1215) ##### 📚 Documentation - **(rust)** drop the restated one-declaration line from the intro by [@​jdx](https://github.com/jdx) in [#​1211](jdx/usage#1211) - **(spec)** complete KDL reference by [@​jdx](https://github.com/jdx) in [#​1214](jdx/usage#1214) ### [`v6.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#600---2026-08-22) [Compare Source](jdx/usage@v5.1.0...v6.0.0) ##### 🚀 Features - **(argv)** add a zero-allocation argv parser by [@​jdx](https://github.com/jdx) in [#​798](jdx/usage#798) - **(argv)** emit a usage spec from static metadata by [@​jdx](https://github.com/jdx) in [#​801](jdx/usage#801) - **(argv)** a bound stops a variadic by [@​jdx](https://github.com/jdx) in [#​826](jdx/usage#826) - **(argv)** route a word that names nothing to the default subcommand by [@​jdx](https://github.com/jdx) in [#​848](jdx/usage#848) - **(argv)** join static tables at compile time by [@​jdx](https://github.com/jdx) in [#​851](jdx/usage#851) - **(argv)** render the usage line, byte-identical to usage-lib's by [@​jdx](https://github.com/jdx) in [#​854](jdx/usage#854) - **(argv)** render `-h`, byte-identical to usage-lib's by [@​jdx](https://github.com/jdx) in [#​860](jdx/usage#860) - **(argv)** render `--help` too, byte-identical to usage-lib's by [@​jdx](https://github.com/jdx) in [#​866](jdx/usage#866) - **(argv)** answer `--help` and `-h` by [@​jdx](https://github.com/jdx) in [#​870](jdx/usage#870) - **(argv)** answer the `help` subcommand by [@​jdx](https://github.com/jdx) in [#​872](jdx/usage#872) - **(argv)** split a command line the way the shell that typed it would by [@​jdx](https://github.com/jdx) in [#​874](jdx/usage#874) - **(argv)** read the cursor's position off a real parse by [@​jdx](https://github.com/jdx) in [#​876](jdx/usage#876) - **(argv)** offer what the reference offers, from compiled tables by [@​jdx](https://github.com/jdx) in [#​877](jdx/usage#877) - **(argv)** generate the shell script each shell wants by [@​jdx](https://github.com/jdx) in [#​887](jdx/usage#887) - **(argv)** let a Rust function answer for a value by [@​jdx](https://github.com/jdx) in [#​888](jdx/usage#888) - **(argv)** write the `run=` a declared completer answers by [@​jdx](https://github.com/jdx) in [#​890](jdx/usage#890) - **(argv)** say what went wrong the way clap says it by [@​jdx](https://github.com/jdx) in [#​895](jdx/usage#895) - **(argv)** suggest what was probably meant by [@​jdx](https://github.com/jdx) in [#​897](jdx/usage#897) - **(argv)** answer `--version`, which an adopter loses on the way from clap by [@​jdx](https://github.com/jdx) in [#​909](jdx/usage#909) - **(argv)** a flag whose value may be left off by [@​jdx](https://github.com/jdx) in [#​969](jdx/usage#969) - **(argv)** take flag-like detached values when declared by [@​jdx](https://github.com/jdx) in [#​1012](jdx/usage#1012) - **(bench)** count what a parse allocates, and stop allocating for commands nobody ran by [@​jdx](https://github.com/jdx) in [#​829](jdx/usage#829) - **(cli)** hold a spec's declaration order, the way clap-sort holds a clap CLI's by [@​jdx](https://github.com/jdx) in [#​915](jdx/usage#915) - **(cli)** parse usage's own command line with the parser usage ships by [@​jdx](https://github.com/jdx) in [#​965](jdx/usage#965) - **(cli)** support long version text by [@​jdx](https://github.com/jdx) in [#​1120](jdx/usage#1120) - **(cli)** check that examples still parse, and let the derive declare them by [@​jdx](https://github.com/jdx) in [#​1168](jdx/usage#1168) - **(cli)** add usage explain by [@​jdx](https://github.com/jdx) in [#​1179](jdx/usage#1179) - **(cli)** add usage diff for spec compatibility checking by [@​jdx](https://github.com/jdx) in [#​1171](jdx/usage#1171) - **(complete)** complete config keys and values from the spec by [@​jdx](https://github.com/jdx) in [#​840](jdx/usage#840) - **(complete)** add async runtime overlays by [@​jdx](https://github.com/jdx) in [#​1060](jdx/usage#1060) - **(complete)** support command value hints by [@​jdx](https://github.com/jdx) in [#​1081](jdx/usage#1081) - **(complete)** add shell quoting filter by [@​jdx](https://github.com/jdx) in [#​1114](jdx/usage#1114) - **(complete)** support full value hint vocabulary by [@​jdx](https://github.com/jdx) in [#​1119](jdx/usage#1119) - **(complete)** expand partial path segments by [@​jdx](https://github.com/jdx) in [#​1128](jdx/usage#1128) - **(complete)** support shell alias registration by [@​jdx](https://github.com/jdx) in [#​1158](jdx/usage#1158) - **(complete)** **breaking** remove the vendored bash-completion copy by [@​jdx](https://github.com/jdx) in [#​1176](jdx/usage#1176) - **(complete)** install a completion script where its shell looks for it by [@​jdx](https://github.com/jdx) in [#​1188](jdx/usage#1188) - **(config)** read config files as a layer by [@​jdx](https://github.com/jdx) in [#​856](jdx/usage#856) - **(config)** explain why a setting has the value it has by [@​jdx](https://github.com/jdx) in [#​857](jdx/usage#857) - **(config)** read a resolution as the types a struct holds by [@​jdx](https://github.com/jdx) in [#​862](jdx/usage#862) - **(config)** generate the settings registry from the spec by [@​jdx](https://github.com/jdx) in [#​864](jdx/usage#864) - **(config)** generate the settings struct a CLI reads by [@​jdx](https://github.com/jdx) in [#​865](jdx/usage#865) - **(config)** hold a value to the choices its setting declares by [@​jdx](https://github.com/jdx) in [#​868](jdx/usage#868) - **(config)** carry a setting's choices into the generated registry by [@​jdx](https://github.com/jdx) in [#​869](jdx/usage#869) - **(config)** say what sort of thing each warning is by [@​jdx](https://github.com/jdx) in [#​873](jdx/usage#873) - **(config)** carry the flags a setting declares into its registry by [@​jdx](https://github.com/jdx) in [#​880](jdx/usage#880) - **(config)** read the command line as a layer by [@​jdx](https://github.com/jdx) in [#​881](jdx/usage#881) - **(config)** compare the flags a spec declares with the flags a CLI binds by [@​jdx](https://github.com/jdx) in [#​884](jdx/usage#884) - **(config)** support optional props and aliases by [@​jdx](https://github.com/jdx) in [#​1134](jdx/usage#1134) - **(config)** read YAML config files by [@​jdx](https://github.com/jdx) in [#​1192](jdx/usage#1192) - **(config)** ask for provenance by key, like a value by [@​jdx](https://github.com/jdx) in [#​1195](jdx/usage#1195) - **(config)** a read that keeps every setting that reads by [@​jdx](https://github.com/jdx) in [#​1196](jdx/usage#1196) - **(config)** close Config derive and spec authoring gaps by [@​jdx](https://github.com/jdx) in [#​1202](jdx/usage#1202) - **(config)** gate deprecated settings by explicit CLI version by [@​jdx](https://github.com/jdx) in [#​1201](jdx/usage#1201) - **(derive)** compile a struct into parse tables and a spec by [@​jdx](https://github.com/jdx) in [#​803](jdx/usage#803) - **(derive)** compile subcommands from an enum by [@​jdx](https://github.com/jdx) in [#​816](jdx/usage#816) - **(derive)** check what a parse cannot decide on its own by [@​jdx](https://github.com/jdx) in [#​817](jdx/usage#817) - **(derive)** nest commands to any depth by [@​jdx](https://github.com/jdx) in [#​818](jdx/usage#818) - **(derive)** declare which flags conflict and which require each other by [@​jdx](https://github.com/jdx) in [#​820](jdx/usage#820) - **(derive)** let a flag displace another, the last one given winning by [@​jdx](https://github.com/jdx) in [#​821](jdx/usage#821) - **(derive)** let a command answer to more than one name by [@​jdx](https://github.com/jdx) in [#​827](jdx/usage#827) - **(derive)** let a variant hold its command in a `Box` by [@​jdx](https://github.com/jdx) in [#​828](jdx/usage#828) - **(derive)** let a field be the type it means by [@​jdx](https://github.com/jdx) in [#​833](jdx/usage#833) - **(derive)** declare the words a value may be by [@​jdx](https://github.com/jdx) in [#​838](jdx/usage#838) - **(derive)** hold the bytes a word arrived as by [@​jdx](https://github.com/jdx) in [#​841](jdx/usage#841) - **(derive)** declare the properties mise patches in by hand by [@​jdx](https://github.com/jdx) in [#​842](jdx/usage#842) - **(derive)** accept a value the OS accepts and UTF-8 does not by [@​jdx](https://github.com/jdx) in [#​844](jdx/usage#844) - **(derive)** share declarations between commands with flatten by [@​jdx](https://github.com/jdx) in [#​852](jdx/usage#852) - **(derive)** say three things about a CLI the spec could and the derive could not by [@​jdx](https://github.com/jdx) in [#​853](jdx/usage#853) - **(derive)** answer a completion request from the binary itself by [@​jdx](https://github.com/jdx) in [#​885](jdx/usage#885) - **(derive)** bind a flag to a setting, from what the parser saw by [@​jdx](https://github.com/jdx) in [#​889](jdx/usage#889) - **(derive)** a setting can be declared wherever a flag is by [@​jdx](https://github.com/jdx) in [#​896](jdx/usage#896) - **(derive)** let a field name the function that completes it by [@​jdx](https://github.com/jdx) in [#​892](jdx/usage#892) - **(derive)** say how an argument relates to `--`, all four ways by [@​jdx](https://github.com/jdx) in [#​900](jdx/usage#900) - **(derive)** a default a collecting field can hold by [@​jdx](https://github.com/jdx) in [#​902](jdx/usage#902) - **(derive)** say what a command does to the world by [@​jdx](https://github.com/jdx) in [#​905](jdx/usage#905) - **(derive)** name a value the way clap names it, and say which usage can read the spec by [@​jdx](https://github.com/jdx) in [#​907](jdx/usage#907) - **(derive)** let `parse()` answer a failure the way a program does by [@​jdx](https://github.com/jdx) in [#​910](jdx/usage#910) - **(derive)** read the package's version, and be called what the binary is called by [@​jdx](https://github.com/jdx) in [#​917](jdx/usage#917) - **(derive)** a command that takes nothing can be written that way by [@​jdx](https://github.com/jdx) in [#​923](jdx/usage#923) - **(derive)** say that a command cannot be run alone, which it knew and did not write by [@​jdx](https://github.com/jdx) in [#​937](jdx/usage#937) - **(derive)** keep command aliases on their args by [@​jdx](https://github.com/jdx) in [#​946](jdx/usage#946) - **(derive)** preserve verbatim doc comments by [@​jdx](https://github.com/jdx) in [#​949](jdx/usage#949) - **(derive)** support path value hints by [@​jdx](https://github.com/jdx) in [#​951](jdx/usage#951) - **(derive)** declare a group where the flags are declared by [@​jdx](https://github.com/jdx) in [#​934](jdx/usage#934) - **(derive)** add value-conditional requirements by [@​jdx](https://github.com/jdx) in [#​1002](jdx/usage#1002) - **(derive)** add skip for fields that are not arguments by [@​jdx](https://github.com/jdx) in [#​1009](jdx/usage#1009) - **(derive)** support inline subcommand fields by [@​jdx](https://github.com/jdx) in [#​1055](jdx/usage#1055) - **(derive)** accept runtime metadata expressions by [@​jdx](https://github.com/jdx) in [#​1056](jdx/usage#1056) - **(derive)** accept clap value attributes by [@​jdx](https://github.com/jdx) in [#​1057](jdx/usage#1057) - **(derive)** parse full argv with program name by [@​jdx](https://github.com/jdx) in [#​1063](jdx/usage#1063) - **(derive)** support clap no binary name by [@​jdx](https://github.com/jdx) in [#​1064](jdx/usage#1064) - **(derive)** support unit command structs by [@​jdx](https://github.com/jdx) in [#​1071](jdx/usage#1071) - **(derive)** reuse args across commands by [@​jdx](https://github.com/jdx) in [#​1076](jdx/usage#1076) - **(derive)** support runtime program identity by [@​jdx](https://github.com/jdx) in [#​1078](jdx/usage#1078) - **(derive)** preserve value enum metadata by [@​jdx](https://github.com/jdx) in [#​1079](jdx/usage#1079) - **(derive)** accept clap field spellings by [@​jdx](https://github.com/jdx) in [#​1086](jdx/usage#1086) - **(derive)** preserve hidden flag aliases by [@​jdx](https://github.com/jdx) in [#​1087](jdx/usage#1087) - **(derive)** resolve relationships through flatten by [@​jdx](https://github.com/jdx) in [#​1088](jdx/usage#1088) - **(derive)** support flattened overrides by [@​jdx](https://github.com/jdx) in [#​1089](jdx/usage#1089) - **(derive)** preserve flattened help headings by [@​jdx](https://github.com/jdx) in [#​1090](jdx/usage#1090) - **(derive)** support clap casing policies by [@​jdx](https://github.com/jdx) in [#​1094](jdx/usage#1094) - **(derive)** bind value enums directly by [@​jdx](https://github.com/jdx) in [#​1110](jdx/usage#1110) - **(derive)** accept portable clap field spellings by [@​jdx](https://github.com/jdx) in [#​1135](jdx/usage#1135) - **(derive)** inherit clap command metadata by [@​jdx](https://github.com/jdx) in [#​1136](jdx/usage#1136) - **(derive)** support clap implicit groups by [@​jdx](https://github.com/jdx) in [#​1137](jdx/usage#1137) - **(derive)** generate command dispatch by [@​jdx](https://github.com/jdx) in [#​1182](jdx/usage#1182) - **(derive)** add usage::Config derive for settings declared in code by [@​jdx](https://github.com/jdx) in [#​1180](jdx/usage#1180) - **(derive)** close remaining PLAN gaps for 6.x by [@​jdx](https://github.com/jdx) in [#​1197](jdx/usage#1197) - **(docs)** support granular help visibility by [@​jdx](https://github.com/jdx) in [#​1107](jdx/usage#1107) - **(docs)** customize subcommand presentation by [@​jdx](https://github.com/jdx) in [#​1108](jdx/usage#1108) - **(docs)** color process-facing help by [@​jdx](https://github.com/jdx) in [#​1111](https://github.com/jdx/usage/pull/1111) - **(docs)** support help width controls by [@​jdx](https://github.com/jdx) in [#​1113](https://github.com/jdx/usage/pull/1113) - **(docs)** support next-line help layout by [@​jdx](https://github.com/jdx) in [#​1117](https://github.com/jdx/usage/pull/1117) - **(docs)** support flattened subcommand help by [@​jdx](https://github.com/jdx) in [#​1118](https://github.com/jdx/usage/pull/1118) - **(docs)** support explicit display order by [@​jdx](https://github.com/jdx) in [#​1121](https://github.com/jdx/usage/pull/1121) - **(docs)** group subcommands under help headings by [@​jdx](https://github.com/jdx) in [#​1153](https://github.com/jdx/usage/pull/1153) - **(docs)** add recursive help by [@​jdx](https://github.com/jdx) in [#​1132](https://github.com/jdx/usage/pull/1132) - **(generate)** add json-schema for a CLI's config file by [@​jdx](https://github.com/jdx) in [#​839](https://github.com/jdx/usage/pull/839) - **(go)** emit Go parse tables from a spec, which is what Go has instead of a derive by [@​jdx](https://github.com/jdx) in [#​931](https://github.com/jdx/usage/pull/931) - **(go)** emit the cold table too, so generated code can apply the rules by [@​jdx](https://github.com/jdx) in [#​959](https://github.com/jdx/usage/pull/959) - **(go)** render the usage line, from a third table that costs nothing unused by [@​jdx](https://github.com/jdx) in [#​964](https://github.com/jdx/usage/pull/964) - **(go)** render a failure as something a person can act on by [@​jdx](https://github.com/jdx) in [#​977](https://github.com/jdx/usage/pull/977) - **(go)** generate a struct per command, and the Parse that fills them by [@​jdx](https://github.com/jdx) in [#​990](https://github.com/jdx/usage/pull/990) - **(go)** answer the completion request a shell sends by [@​jdx](https://github.com/jdx) in [#​1005](https://github.com/jdx/usage/pull/1005) - **(go)** enforce value-conditional requirements by [@​jdx](https://github.com/jdx) in [#​1003](https://github.com/jdx/usage/pull/1003) - **(help)** line the flag column up, and give the short page a column at all by [@​jdx](https://github.com/jdx) in [#​912](https://github.com/jdx/usage/pull/912) - **(help)** list the flags a command inherits by [@​jdx](https://github.com/jdx) in [#​913](https://github.com/jdx/usage/pull/913) - **(help)** list `--help` and `--version`, which every page answers by [@​jdx](https://github.com/jdx) in [#​914](https://github.com/jdx/usage/pull/914) - **(lib)** add usage-rs facade by [@​jdx](https://github.com/jdx) in [#​963](https://github.com/jdx/usage/pull/963) - **(lib)** ship usage-rs as the one-crate rust default by [@​jdx](https://github.com/jdx) in [#​1041](https://github.com/jdx/usage/pull/1041) - **(parse)** support inferred prefixes by [@​jdx](https://github.com/jdx) in [#​1080](https://github.com/jdx/usage/pull/1080) - **(parse)** support arg required else help by [@​jdx](https://github.com/jdx) in [#​1093](https://github.com/jdx/usage/pull/1093) - **(parse)** add narrow token boundary controls by [@​jdx](https://github.com/jdx) in [#​1097](https://github.com/jdx/usage/pull/1097) - **(parse)** preserve trailing delimiters by [@​jdx](https://github.com/jdx) in [#​1098](https://github.com/jdx/usage/pull/1098) - **(parse)** add scalar repeat policy by [@​jdx](https://github.com/jdx) in [#​1102](https://github.com/jdx/usage/pull/1102) - **(parse)** add subcommand requirement policy by [@​jdx](https://github.com/jdx) in [#​1103](https://github.com/jdx/usage/pull/1103) - **(parse)** add argument subcommand conflicts by [@​jdx](https://github.com/jdx) in [#​1104](https://github.com/jdx/usage/pull/1104) - **(parse)** add subcommand value precedence by [@​jdx](https://github.com/jdx) in [#​1105](https://github.com/jdx/usage/pull/1105) - **(parse)** support missing optional positionals by [@​jdx](https://github.com/jdx) in [#​1106](https://github.com/jdx/usage/pull/1106) - **(parse)** support optional flag values by [@​jdx](https://github.com/jdx) in [#​1109](https://github.com/jdx/usage/pull/1109) - **(parse)** support custom help and version actions by [@​jdx](https://github.com/jdx) in [#​1123](https://github.com/jdx/usage/pull/1123) - **(parse)** accept explicit boolean values by [@​jdx](https://github.com/jdx) in [#​1124](https://github.com/jdx/usage/pull/1124) - **(parse)** support non-strict choices by [@​jdx](https://github.com/jdx) in [#​1127](https://github.com/jdx/usage/pull/1127) - **(parse)** support ordered environment fallbacks by [@​jdx](https://github.com/jdx) in [#​1130](https://github.com/jdx/usage/pull/1130) - **(parse)** warn at runtime when a deprecated declaration is used by [@​jdx](https://github.com/jdx) in [#​1186](https://github.com/jdx/usage/pull/1186) - **(spec)** support flag relationships by [@​jdx](https://github.com/jdx) in [#​793](https://github.com/jdx/usage/pull/793) - **(spec)** add help\_heading, and render it by [@​jdx](https://github.com/jdx) in [#​802](https://github.com/jdx/usage/pull/802) - **(spec)** allow a mount at the top level by [@​jdx](https://github.com/jdx) in [#​806](https://github.com/jdx/usage/pull/806) - **(spec)** make unknown flags configurable, and keep them as values by [@​jdx](https://github.com/jdx) in [#​810](https://github.com/jdx/usage/pull/810) - **(spec)** add `conflicts` to flags by [@​jdx](https://github.com/jdx) in [#​819](https://github.com/jdx/usage/pull/819) - **(spec)** say that one flag needs another, which nothing here could by [@​jdx](https://github.com/jdx) in [#​925](https://github.com/jdx/usage/pull/925) - **(spec)** **breaking** a group, for the rule that no single flag can state by [@​jdx](https://github.com/jdx) in [#​927](https://github.com/jdx/usage/pull/927) - **(spec)** a flag that has to be given on its own by [@​jdx](https://github.com/jdx) in [#​941](https://github.com/jdx/usage/pull/941) - **(spec)** split a value the way clap splits one by [@​jdx](https://github.com/jdx) in [#​961](https://github.com/jdx/usage/pull/961) - **(spec)** add value-conditional requirements by [@​jdx](https://github.com/jdx) in [#​1001](https://github.com/jdx/usage/pull/1001) - **(spec)** refuse a detached value when require\_equals is set by [@​jdx](https://github.com/jdx) in [#​1013](https://github.com/jdx/usage/pull/1013) - **(spec)** bind a value when a flag is given with none by [@​jdx](https://github.com/jdx) in [#​1015](https://github.com/jdx/usage/pull/1015) - **(spec)** forward unmatched words as an external subcommand by [@​jdx](https://github.com/jdx) in [#​1021](https://github.com/jdx/usage/pull/1021) - **(spec)** bind a default when another flag is given by [@​jdx](https://github.com/jdx) in [#​1023](https://github.com/jdx/usage/pull/1023) - **(spec)** add portable expression validation by [@​jdx](https://github.com/jdx) in [#​1037](https://github.com/jdx/usage/pull/1037) - **(spec)** add borrowed metadata overlays by [@​jdx](https://github.com/jdx) in [#​1059](https://github.com/jdx/usage/pull/1059) - **(spec)** omit versions from metadata views by [@​jdx](https://github.com/jdx) in [#​1066](https://github.com/jdx/usage/pull/1066) - **(spec)** support positional conflicts and groups by [@​jdx](https://github.com/jdx) in [#​1085](https://github.com/jdx/usage/pull/1085) - **(spec)** add fixed arity value names by [@​jdx](https://github.com/jdx) in [#​1099](https://github.com/jdx/usage/pull/1099) - **(spec)** complete relationship families by [@​jdx](https://github.com/jdx) in [#​1100](https://github.com/jdx/usage/pull/1100) - **(spec)** expose package metadata by [@​jdx](https://github.com/jdx) in [#​1116](https://github.com/jdx/usage/pull/1116) - **(spec)** add deprecation milestones by [@​jdx](https://github.com/jdx) in [#​1129](https://github.com/jdx/usage/pull/1129) - **(spec)** add executable views by [@​jdx](https://github.com/jdx) in [#​1143](https://github.com/jdx/usage/pull/1143) - **(spec)** add deprecated config environment aliases by [@​jdx](https://github.com/jdx) in [#​1159](https://github.com/jdx/usage/pull/1159) - **(spec)** declare source\_code\_link\_template on the derive by [@​jdx](https://github.com/jdx) in [#​1184](https://github.com/jdx/usage/pull/1184) - **(spec)** answer **usage\_spec** from a binary's own tables by [@​jdx](https://github.com/jdx) in [#​1183](https://github.com/jdx/usage/pull/1183) - **(spec)** reusable flag declarations with flagset and use by [@​jdx](https://github.com/jdx) in [#​1170](https://github.com/jdx/usage/pull/1170) - **(spec)** **breaking** lower the derive's flatten into a flagset by [@​jdx](https://github.com/jdx) in [#​1172](https://github.com/jdx/usage/pull/1172) - **(test)** a test harness for an adopter's own suite by [@​jdx](https://github.com/jdx) in [#​1181](https://github.com/jdx/usage/pull/1181) ##### 🐛 Bug Fixes - **(argv)** stop a repeatable flag from eating a positional by [@​jdx](https://github.com/jdx) in [#​799](https://github.com/jdx/usage/pull/799) - **(argv)** inherit `unknown_flags`, which reached one command out of a tree by [@​jdx](https://github.com/jdx) in [#​939](https://github.com/jdx/usage/pull/939) - **(argv)** reject duplicate flags by [@​jdx](https://github.com/jdx) in [#​945](https://github.com/jdx/usage/pull/945) - **(argv)** show choices when a subcommand is required by [@​jdx](https://github.com/jdx) in [#​947](https://github.com/jdx/usage/pull/947) - **(argv)** a bare `-` binds where it was typed by [@​jdx](https://github.com/jdx) in [#​986](https://github.com/jdx/usage/pull/986) - **(argv)** put zsh's magic comment first, and print fish's candidates as data by [@​jdx](https://github.com/jdx) in [#​1033](https://github.com/jdx/usage/pull/1033) - **(ci)** unblock releases by cutting usage-derive's dev-dependency by [@​jdx](https://github.com/jdx) in [#​811](https://github.com/jdx/usage/pull/811) - **(ci)** check the version the crates promise, and promise one that is true by [@​jdx](https://github.com/jdx) in [#​918](https://github.com/jdx/usage/pull/918) - **(clap)** say what clap would do with an unknown flag by [@​jdx](https://github.com/jdx) in [#​899](https://github.com/jdx/usage/pull/899) - **(cli)** recognize about as root command help by [@​jdx](https://github.com/jdx) in [#​794](https://github.com/jdx/usage/pull/794) - **(complete)** resolve config keys through aliases and renames by [@​jdx](https://github.com/jdx) in [#​1169](https://github.com/jdx/usage/pull/1169) - **(config)** accept case-insensitive boolean words by [@​jdx](https://github.com/jdx) in [#​1207](https://github.com/jdx/usage/pull/1207) - **(derive)** let a `--`-only argument follow a variadic by [@​jdx](https://github.com/jdx) in [#​823](https://github.com/jdx/usage/pull/823) - **(derive)** three more descriptions a spec keeps and the derive lost by [@​jdx](https://github.com/jdx) in [#​861](https://github.com/jdx/usage/pull/861) - **(derive)** name the mistake when `settings` has nothing to collect by [@​jdx](https://github.com/jdx) in [#​904](https://github.com/jdx/usage/pull/904) - **(derive)** emit the tables beside the user's types, not in a module above them by [@​jdx](https://github.com/jdx) in [#​938](https://github.com/jdx/usage/pull/938) - **(derive)** a global flag may be given once per command, not once per line by [@​jdx](https://github.com/jdx) in [#​991](https://github.com/jdx/usage/pull/991) - **(derive)** separate value metadata from parsing by [@​jdx](https://github.com/jdx) in [#​1054](https://github.com/jdx/usage/pull/1054) - **(derive)** make defaulted fields optional in metadata by [@​jdx](https://github.com/jdx) in [#​1065](https://github.com/jdx/usage/pull/1065) - **(derive)** isolate process exit from adopters by [@​jdx](https://github.com/jdx) in [#​1139](https://github.com/jdx/usage/pull/1139) - **(derive)** propagate redeclared global values by [@​jdx](https://github.com/jdx) in [#​1140](https://github.com/jdx/usage/pull/1140) - **(derive)** preserve set-false actions by [@​jdx](https://github.com/jdx) in [#​1156](https://github.com/jdx/usage/pull/1156) - **(derive)** name the count type in standing presence checks by [@​jdx](https://github.com/jdx) in [#​1205](https://github.com/jdx/usage/pull/1205) - **(docs)** link multi-word commands to their real source files by [@​jdx](https://github.com/jdx) in [#​845](https://github.com/jdx/usage/pull/845) - **(docs)** link every command to the file that implements it by [@​jdx](https://github.com/jdx) in [#​846](https://github.com/jdx/usage/pull/846) - **(docs)** keep hidden entries out of help by [@​jdx](https://github.com/jdx) in [#​859](https://github.com/jdx/usage/pull/859) - **(docs)** list visible flag aliases by [@​jdx](https://github.com/jdx) in [#​1112](https://github.com/jdx/usage/pull/1112) - **(help)** a command's page should say what that command does by [@​jdx](https://github.com/jdx) in [#​911](https://github.com/jdx/usage/pull/911) - **(help)** a declared name is not a short form, and blank help is no help by [@​jdx](https://github.com/jdx) in [#​916](https://github.com/jdx/usage/pull/916) - **(help)** render the page for the mount the words reached by [@​jdx](https://github.com/jdx) in [#​928](https://github.com/jdx/usage/pull/928) - **(help)** a description ending in a break adds no blank line by [@​jdx](https://github.com/jdx) in [#​970](https://github.com/jdx/usage/pull/970) - **(lib)** validate every variadic fallback by [@​jdx](https://github.com/jdx) in [#​1049](https://github.com/jdx/usage/pull/1049) - **(parse)** keep every `--` after the first by [@​jdx](https://github.com/jdx) in [#​809](https://github.com/jdx/usage/pull/809) - **(parse)** stop losing a flag that is missing its value by [@​jdx](https://github.com/jdx) in [#​807](https://github.com/jdx/usage/pull/807) - **(parse)** answer the five vectors the reference implementation was failing by [@​jdx](https://github.com/jdx) in [#​930](https://github.com/jdx/usage/pull/930) - **(parse)** **breaking** a command that needs a subcommand says so by [@​jdx](https://github.com/jdx) in [#​992](https://github.com/jdx/usage/pull/992) - **(parse)** keep optional validation lint-clean by [@​jdx](https://github.com/jdx) in [#​1141](https://github.com/jdx/usage/pull/1141) - **(parse)** honor separator after automatic args by [@​jdx](https://github.com/jdx) in [#​1164](https://github.com/jdx/usage/pull/1164) - **(parse)** let a bundle contain a supplied short by [@​jdx](https://github.com/jdx) in [#​1175](https://github.com/jdx/usage/pull/1175) - **(spec)** make the config block survive being written out by [@​jdx](https://github.com/jdx) in [#​832](https://github.com/jdx/usage/pull/832) - **(spec)** apply default\_subcommand only at the root by [@​jdx](https://github.com/jdx) in [#​850](https://github.com/jdx/usage/pull/850) - **(spec)** split a clap default by the delimiter clap splits it by by [@​jdx](https://github.com/jdx) in [#​901](https://github.com/jdx/usage/pull/901) - **(spec)** rank a subcommand name above another command's alias by [@​jdx](https://github.com/jdx) in [#​967](https://github.com/jdx/usage/pull/967) - **(spec)** preserve clap value count bounds by [@​jdx](https://github.com/jdx) in [#​1032](https://github.com/jdx/usage/pull/1032) - **(spec)** deduplicate derived completers by [@​jdx](https://github.com/jdx) in [#​1072](https://github.com/jdx/usage/pull/1072) - **(spec)** canonicalize derived kdl by [@​jdx](https://github.com/jdx) in [#​1095](https://github.com/jdx/usage/pull/1095) ##### 🚜 Refactor - **(deps)** **breaking** stop shipping features and crates nobody uses by [@​jdx](https://github.com/jdx) in [#​1185](https://github.com/jdx/usage/pull/1185) - **(deps)** drop heck from usage-derive by [@​jdx](https://github.com/jdx) in [#​1187](https://github.com/jdx/usage/pull/1187) - **(deps)** take expr-lang without the builtins a spec cannot reach by [@​jdx](https://github.com/jdx) in [#​1191](https://github.com/jdx/usage/pull/1191) ##### 📚 Documentation - **(plan)** tick landed clap gaps and stop quoting vector counts by [@​jdx](https://github.com/jdx) in [#​1027](https://github.com/jdx/usage/pull/1027) - correct current Rust limitations by [@​jdx](https://github.com/jdx) in [#​1029](https://github.com/jdx/usage/pull/1029) - audit 6.x release documentation by [@​jdx](https://github.com/jdx) in [#​1084](https://github.com/jdx/usage/pull/1084) - add third-party license notices by [@​jdx](https://github.com/jdx) in [#​1174](https://github.com/jdx/usage/pull/1174) ##### ⚡ Performance - **(derive)** fill the partial through \&mut instead of returning it by [@​jdx](https://github.com/jdx) in [#​980](https://github.com/jdx/usage/pull/980) - **(derive)** hold one subcommand's partial, not every subcommand's by [@​jdx](https://github.com/jdx) in [#​981](https://github.com/jdx/usage/pull/981) - **(derive)** drop proc-macro-crate transitive deps by [@​jdx](https://github.com/jdx) in [#​1042](https://github.com/jdx/usage/pull/1042) ##### 🧪 Testing - **(clap)** preserve choices in external adopter probes by [@​jdx](https://github.com/jdx) in [#​1157](https://github.com/jdx/usage/pull/1157) - **(corpus)** pin what completes where the cursor is by [@​jdx](https://github.com/jdx) in [#​998](https://github.com/jdx/usage/pull/998) - **(derive)** cover verbatim doc compatibility by [@​jdx](https://github.com/jdx) in [#​1092](https://github.com/jdx/usage/pull/1092) - **(docs)** preserve fleet footer spacing by [@​jdx](https://github.com/jdx) in [#​1142](https://github.com/jdx/usage/pull/1142) - **(fleet)** refresh typed adopter fixtures by [@​jdx](https://github.com/jdx) in [#​1115](https://github.com/jdx/usage/pull/1115) - **(parse)** cover mounted command discovery by [@​jdx](https://github.com/jdx) in [#​1131](https://github.com/jdx/usage/pull/1131) - **(parse)** add clap micro-conformance by [@​jdx](https://github.com/jdx) in [#​1133](https://github.com/jdx/usage/pull/1133) - **(spec)** import the argv questions clap's suite answers and ours did not by [@​jdx](https://github.com/jdx) in [#​926](https://github.com/jdx/usage/pull/926) - **(spec)** verify portable parser settings by [@​jdx](https://github.com/jdx) in [#​1053](https://github.com/jdx/usage/pull/1053) ##### 🛡️ Security - **(config)** resolve settings from layers, with provenance by [@​jdx](https://github.com/jdx) in [#​849](https://github.com/jdx/usage/pull/849) - **(config)** read the environment as a layer by [@​jdx](https://github.com/jdx) in [#​867](https://github.com/jdx/usage/pull/867) - **(config)** give a deprecation notice from anywhere along a rename chain by [@​jdx](https://github.com/jdx) in [#​893](https://github.com/jdx/usage/pull/893) - **(derive)** keep parsed fields live for lints by [@​jdx](https://github.com/jdx) in [#​1138](https://github.com/jdx/usage/pull/1138) - **(docs)** render the config block by [@​jdx](https://github.com/jdx) in [#​837](https://github.com/jdx/usage/pull/837) - **(go)** render the page `-h` prints, matching usage-lib on all 211 of mise's by [@​jdx](https://github.com/jdx) in [#​974](https://github.com/jdx/usage/pull/974) - **(go)** render `--help` too, matching usage-lib on all 211 of mise's long pages by [@​jdx](https://github.com/jdx) in [#​975](https://github.com/jdx/usage/pull/975) - **(parse)** require exact command and flag names by [@​jdx](https://github.com/jdx) in [#​1096](https://github.com/jdx/usage/pull/1096) - **(spec)** the config vocabulary by [@​jdx](https://github.com/jdx) in [#​835](https://github.com/jdx/usage/pull/835) ##### 🔍 Other Changes - **(docs)** remove stale mise spec fixture by [@​jdx](https://github.com/jdx) in [#​1200](https://github.com/jdx/usage/pull/1200) - **(perf)** say when the clap ratio slides, and record why the derive is stricter by [@​jdx](https://github.com/jdx) in [#​996](https://github.com/jdx/usage/pull/996) - agent/complete files by [@​jdx](https://github.com/jdx) in [#​883](https://github.com/jdx/usage/pull/883) ##### 📦️ Dependency Updates - update rust crate syn to v3 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​808](https://github.com/jdx/usage/pull/808) - update rust crate toml to v1 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​1016](https://github.com/jdx/usage/pull/1016) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODguMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWFqb3IiXX0=-->


Stacked on #797 — review that one first; this PR's diff against
mainwill include it until it merges.What this is
argv/— a new crate,usage-argv, implementing the binding half of the grammar from #797. No command tree, no allocation, one pass overargv.Not published (name is reserved on crates.io at 0.0.0), and deliberately outside the shared-version release cycle, the way
clap_usagealready is.Two design decisions worth a look
Events, not a map. Parsing yields
Event::{Command, Flag, Arg}rather than returning a structure. A map would have to allocate and then be read back out; an event can be assigned straight into a struct field by generated code. Same reasoning as serde deserializing into your type instead of into aValue.Values are
&[u8]. Borrowed fromargv, converted by the caller withas_str. Slicing anOsStrinto&strpieces needs either an allocation orunsafe, and this crate forbidsunsafe. The upside is that a non-UTF-8 command line still parses — flags match, subcommands route — and only the values actually looked at can fail to convert, which is where that failure belongs.Tables are borrowed slices (
&'a [&'a Flag<'a>]) so a derive can emit the whole tree asstaticdata. Tables andargvcarry separate lifetimes; a single lifetime compiled but forcedargvto be'static, which a doctest caught.Scope
Binding only.
required,choices,envfallback, defaults,var_min/var_max, andoverridesall happen after the last token is read and need to know a value's type, so they belong to the layer that owns the target struct. Keeping them out is what keeps this loop small.How it's verified
The corpus.
conformance/gained a second runner, so the same vectors now exercise both parsers. usage-argv answers 61 of 83; the remaining 22 are post-binding and report why they're exempt, with the count asserted so the exempt set can't quietly grow. A separate test asserts that all 15 vectors where usage-lib diverges from the grammar are ones this parser gets right — that was the point of writing the grammar down.Since nothing emits tables yet, the harness builds them from a
Specand leaks them. A test process building a few small tables is the one place where leaking is the simplest correct answer; generated code has no such problem.Zero allocation.
argv/tests/no_alloc.rsarms a counting global allocator around parses of ten realistic command lines and four failing ones, and asserts zero. It also asserts the counter observes a deliberate allocation, so the test can't pass vacuously.One wrinkle worth knowing if you write similar tests: the file holds exactly one
#[test], because the counter is global and a sibling test building aVecon another thread showed up as four phantom allocations here.One grammar change
Implementing this surfaced a rule the doc didn't state: a flag with a variadic argument (
--include <pattern>...) consumes following tokens until one is flag-like. I've added it todocs/spec/argv.md, including the note that it is greedy and will eat positionals — inherent to the feature, ended explicitly with--.Next
A derive that emits these tables, and then a benchmark against a clap-shaped equivalent at mise's scale, which is the gate for whether this is worth continuing.
AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.
Note
Medium Risk
New public parser surface and grammar-aligned behavior that differs from usage-lib on recorded divergences; release script changes affect publishing, but no changes to existing usage-lib parse paths in this diff.
Overview
Introduces
usage-argv, a dependency-free crate that implements the argv binding half of the usage grammar in one pass with no heap allocation. Parsing is driven by staticCommand/Flag/Argtables (intended for future derive output) and streamsEventvalues with&[u8]payloads instead of building a map.Conformance gains a second runner (
conformance/src/argv.rs+conformance/tests/argv.rs) that builds leaked tables from corpus specs and asserts 62 in-scope binding vectors pass, including cases where usage-lib diverges; post-binding features (required,choices,env, defaults, etc.) are explicitly out of scope with a fixed count.argv/tests/no_alloc.rsenforces zero allocations during parse via a counting global allocator.docs/spec/argv.mddocuments greedy variadic flag value consumption and notes dual-parser corpus testing.tasks/release-plzpublishesusage-argvwith the shared version and includesargv/**in changelog path filters.Reviewed by Cursor Bugbot for commit 180aec7. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
--handling.Documentation
Tests