feat(derive): accept a value the OS accepts and UTF-8 does not - #844
Conversation
📝 WalkthroughWalkthroughThe PR adds safe conversion from argument bytes to ChangesNon-UTF-8 OS value preservation
Estimated code review effort: 3 (Moderate) | ~25 minutes Mergeability Score: ⚪ Minimal · up to The PR adds byte-exact OS argument handling with bounded platform-specific behavior and documented tests; no actionable merge-blocking risk remains beyond a minor documentation-formatting cleanup. Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant CLI
participant Parser
participant field_final
participant os_string_from_bytes
participant TypedField
CLI->>Parser: provide argument bytes
Parser->>field_final: pass parsed bytes
field_final->>os_string_from_bytes: convert PathBuf or OsString bytes
os_string_from_bytes-->>field_final: return OS value or InvalidValue
field_final-->>TypedField: assign required, optional, or repeated value
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 countsThe comparison never ran — an earlier step failed.
|
Greptile SummaryThe PR safely completes the prior fix by preserving arbitrary argv bytes for
Confidence Score: 5/5The PR appears safe to merge. The previously reported unchecked Windows conversion has been removed, and no blocking failure remains. Important Files Changed
Reviews (6): Last reviewed commit: "fix(argv): make the byte conversion soun..." | Re-trigger Greptile |
|
Addressed in the latest push. The P1 was correct and the fix was to remove the `unsafe`, not to justify it better. A safe function taking a `Vec` cannot enforce a "these came from `as_encoded_bytes`" precondition, so no amount of documentation made it sound. On Unix an `OsString` is an arbitrary byte sequence, so `OsString::from_vec` is exact and safe — that is the case that matters, since non-UTF-8 filenames are ordinary there. On Windows the bytes go through UTF-8 and one that will not convert comes back in the `Err` to be reported. The crate `forbid(unsafe_code)`s again. Byte-exactness is still cheaper than the text path: 553 instructions per parse against 674. AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable. |
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 6aae617. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@PLAN.md`:
- Around line 140-148: Dedent the paragraph beginning “And with no unsafe
anywhere.” so it is rendered as prose within the surrounding checklist item
rather than as an indented code block, while preserving its text and paragraph
structure.
🪄 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: a2a656a6-5bab-46dc-a2ab-33cf7a23d04c
📒 Files selected for processing (6)
.github/workflows/test.ymlPLAN.mdargv/src/lib.rsconformance/tests/typed.rsderive/src/codegen.rsderive/src/model.rs
| **And with no `unsafe` anywhere.** On Unix an `OsString` is an arbitrary byte sequence, | ||
| so this is the safe `OsString::from_vec` and every byte survives — which is the case that | ||
| matters, since non-UTF-8 filenames are ordinary there. Windows was going to need | ||
| `from_encoded_bytes_unchecked`, and jdx approved that, but a *safe* function taking a | ||
| `Vec<u8>` cannot enforce its precondition: there is no way to know the bytes came from | ||
| `as_encoded_bytes` rather than from anywhere else, and a safe function whose precondition | ||
| a caller can violate is unsound however carefully today's callers behave. Greptile flagged | ||
| exactly that on #844. So Windows goes through UTF-8 and reports what will not convert, | ||
| which gives up only an unpaired-surrogate argument there. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Render this text as prose.
Line 140 starts an indented code block after the blank line. Markdownlint reports MD046. Dedent this paragraph so it remains part of the checklist item.
Proposed fix
- **And with no `unsafe` anywhere.** On Unix an `OsString` is an arbitrary byte sequence,
- so this is the safe `OsString::from_vec` and every byte survives — which is the case that
- matters, since non-UTF-8 filenames are ordinary there. Windows was going to need
- `from_encoded_bytes_unchecked`, and jdx approved that, but a *safe* function taking a
- `Vec<u8>` cannot enforce its precondition: there is no way to know the bytes came from
- `as_encoded_bytes` rather than from anywhere else, and a safe function whose precondition
- a caller can violate is unsound however carefully today's callers behave. Greptile flagged
- exactly that on `#844`. So Windows goes through UTF-8 and reports what will not convert,
- which gives up only an unpaired-surrogate argument there.
+ **And with no `unsafe` anywhere.** On Unix an `OsString` is an arbitrary byte sequence,
+ so this is the safe `OsString::from_vec` and every byte survives — which is the case that
+ matters, since non-UTF-8 filenames are ordinary there. Windows was going to need
+ `from_encoded_bytes_unchecked`, and jdx approved that, but a *safe* function taking a
+ `Vec<u8>` cannot enforce its precondition: there is no way to know the bytes came from
+ `as_encoded_bytes` rather than from anywhere else, and a safe function whose precondition
+ a caller can violate is unsound however carefully today's callers behave. Greptile flagged
+ exactly that on `#844`. So Windows goes through UTF-8 and reports what will not convert,
+ which gives up only an unpaired-surrogate argument there.📝 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.
| **And with no `unsafe` anywhere.** On Unix an `OsString` is an arbitrary byte sequence, | |
| so this is the safe `OsString::from_vec` and every byte survives — which is the case that | |
| matters, since non-UTF-8 filenames are ordinary there. Windows was going to need | |
| `from_encoded_bytes_unchecked`, and jdx approved that, but a *safe* function taking a | |
| `Vec<u8>` cannot enforce its precondition: there is no way to know the bytes came from | |
| `as_encoded_bytes` rather than from anywhere else, and a safe function whose precondition | |
| a caller can violate is unsound however carefully today's callers behave. Greptile flagged | |
| exactly that on #844. So Windows goes through UTF-8 and reports what will not convert, | |
| which gives up only an unpaired-surrogate argument there. | |
| **And with no `unsafe` anywhere.** On Unix an `OsString` is an arbitrary byte sequence, | |
| so this is the safe `OsString::from_vec` and every byte survives — which is the case that | |
| matters, since non-UTF-8 filenames are ordinary there. Windows was going to need | |
| `from_encoded_bytes_unchecked`, and jdx approved that, but a *safe* function taking a | |
| `Vec<u8>` cannot enforce its precondition: there is no way to know the bytes came from | |
| `as_encoded_bytes` rather than from anywhere else, and a safe function whose precondition | |
| a caller can violate is unsound however carefully today's callers behave. Greptile flagged | |
| exactly that on #844. So Windows goes through UTF-8 and reports what will not convert, | |
| which gives up only an unpaired-surrogate argument there. |
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 140-140: Code block style
Expected: fenced; Actual: indented
(MD046, code-block-style)
🤖 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 `@PLAN.md` around lines 140 - 148, Dedent the paragraph beginning “And with no
unsafe anywhere.” so it is rendered as prose within the surrounding checklist
item rather than as an indented code block, while preserving its text and
paragraph structure.
Source: Linters/SAST tools
6aae617 to
605080a
Compare
A value used to reach its field through `from_utf8_lossy`, so a path with a stray byte in it became a path with `U+FFFD` in it — a different file, silently. That is the worst shape a bug can take: no error, wrong answer. The partial holds the bytes now and the conversion happens once, where the struct is built. `apply` cannot report anything — it answers whether an event belonged to this command — which is why the bytes have to travel that far before anyone can complain about them. A word that is not UTF-8 is an `InvalidValue` naming the field, showing the value lossily *for the message only*, and saying what was wrong with it. It also retires a hazard review raised on the last PR: `String` was recognised by how it was written so that its value could be moved rather than converted, which broke for an adopter who shadowed the name. There is no identity case left — everything converts — so recognising the spelling only skips a second step now, and getting it wrong is a compile error rather than a mangled value. Costs +656 instructions (1.6%) and one allocation against main, measured on the same fixture: the collecting path rebuilds a `Vec<String>` from bytes rather than moving one. That is what not corrupting a value is worth. Reporting such a value is not the same as accepting it, and accepting it needs `OsStr::from_encoded_bytes_unchecked` — `unsafe`, in a crate that has none. The call would be sound and PLAN.md says why, but that is jdx`s call rather than mine. --- <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how all CLI string values are stored and converted during parse; behavior improves for invalid UTF-8 but touches hot derive codegen paths with a small measured perf cost. > > **Overview** > **Argv values are kept as raw bytes through binding** instead of being turned into strings with `from_utf8_lossy`. The derive partial now stores `Vec<u8>` for string-like fields; conversion to `String` / `FromStr` types happens once in `build`, where invalid UTF-8 becomes **`InvalidValue`** (the error message may show the value lossily for display only). > > **`value_enum` / `choices` checks** skip bytes that are not valid UTF-8 so users get a UTF-8 error instead of a misleading **`InvalidChoice`** list. > > The old **`String` identity fast path** is removed—every typed field goes through the same UTF-8 validation path. **Defaults and `env` fallbacks** write UTF-8 bytes into the partial to match. > > Conformance tests cover non-UTF-8 `--out` paths and enumerated `--shell` values. **PLAN.md** marks “report, don’t mangle” done and notes accepting exact non-UTF-8 paths is still future work. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ea7b427. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> ## Cost Measured on the mise-scale shadow (`use -g node@20`), differencing two runs of the same binary against the same fixture on `main`: | | main | this branch | |---|---:|---:| | instructions, cold parse | 39,828 | 40,484 | | allocations, bound parse | 3 | 4 | | allocations, bare parse | 0 | 0 | +656 instructions, or 1.6%, and one allocation — the `Vec<u8>` a value is held in before it becomes a `String`. That is the price of not silently substituting a different filename, and it is paid only by invocations that carry a value. ## What this does not do A non-UTF-8 value is *reported*, not accepted: `PathBuf` on Unix could hold those bytes faithfully, but constructing an `OsStr` from them needs `OsStr::from_encoded_bytes_unchecked`, and usage-argv has no `unsafe` in it today. The bytes do come from `as_encoded_bytes` in this same process and every split the parser makes is at an ASCII byte, so the call would be sound — but that is a policy decision about the crate rather than a detail of this change, so this branch takes the conservative half and names the field in the error instead. **Resolved in jdx#844**, which is stacked on this one: jdx approved the `unsafe`, and it then turned out to be needed only on Windows — on Unix the safe `OsString::from_vec` does the job. A `PathBuf` field there accepts the bytes exactly, and does so for 567 instructions against a `String` field's 660. *AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable.* <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Preserved command-line, default, and environment values without lossy UTF-8 replacement. * Added clear validation errors for invalid UTF-8 values, including typed options, paths, strings, and collections. * Improved distinction between invalid values and valid text that does not match available choices. * **Documentation** * Updated documentation to explain byte-preserving value handling and current limitations. * **Tests** * Added coverage for valid paths and text, invalid UTF-8 input, enum validation, and field-specific errors. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Reporting a non-UTF-8 value was the safe half of the fix. It is not the whole one: `/tmp/\xff` is a filename the operating system accepts, and a CLI that cannot receive one cannot open the file. A `PathBuf` or `OsString` field now takes the bytes exactly. `usage_argv::os_string_from_bytes` is the reverse of `as_encoded_bytes`. On Unix it is the *safe* `OsString::from_vec` — an `OsString` there is an arbitrary byte sequence, so the conversion is total and no `unsafe` arises. Only Windows needs `from_encoded_bytes_unchecked`, because WTF-8 makes the conversion partial. That call rests on an invariant, now written where it can be checked: every sub-slice the parser produces is cut at an ASCII byte — after `-`, after `--`, at `=`, and between the letters of a short-flag cluster — and an ASCII byte never occurs inside a multi-byte sequence. The one way to break it is a non-ASCII `Flag::shorts` entry, which would let a cluster split mid-character. The derive already refused that; the rule is now documented as load-bearing on both sides and has the test it was missing. The crate goes from `forbid(unsafe_code)` to `deny`, so the single audited exception has to name itself rather than the crate pretending it has none. Parsing still contains no `unsafe` at all. Byte-exactness is cheaper than the text path, not dearer: a `PathBuf` field costs 567 instructions per parse against a `String` field's 660, because it skips the UTF-8 validation pass, and both allocate once. The gate fixture cannot show this — a spec carries no Rust types, so every shadow field is a `String` — so it is measured directly. A `String` field still reports rather than substitutes, which is the right answer for a type that cannot hold those bytes. CI gains a Windows compile check, since every test here is `#[cfg(unix)]` and nothing in the pipeline was building the branch that carries the `unsafe`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`os_string_from_bytes` was a safe function reaching `from_encoded_bytes_unchecked` on Windows, with the precondition — bytes that came from `as_encoded_bytes`, cut only at ASCII — documented and enforced by nothing. It takes a `Vec<u8>` from a safe caller, so there is no way to know where the bytes came from, and a safe function whose precondition a caller can violate is unsound however carefully today's callers behave. Greptile was right to call it a P1. Fixed by not needing the `unsafe` at all. On Unix an `OsString` is an arbitrary byte sequence, so the safe `OsString::from_vec` is exact — which is the case that matters, since non-UTF-8 filenames are ordinary there. On Windows the bytes go through UTF-8 and one that will not convert is handed back in the `Err`, to be reported like any other unconvertible value. What that gives up is a Windows argument containing an unpaired surrogate; what it buys is that the crate forbids `unsafe` again rather than denying it with an exception. The bytes come back in the `Err` rather than being cloned for the message, as `String::from_utf8` does, so the failure path costs nothing on the path that succeeds. The `Flag::shorts` ASCII rule is no longer a soundness matter and no longer claims to be — it stays because a non-ASCII short cannot be matched, and its test stays with it. Co-Authored-By: Claude Opus 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=-->

Reporting a non-UTF-8 value was the safe half of the fix. It is not the whole one:
/tmp/\xffis a filename the operating system accepts, and a CLI that cannot receiveone cannot open the file. A
PathBuforOsStringfield now takes the bytes exactly.usage_argv::os_string_from_bytesis the reverse ofas_encoded_bytes. On Unix it isthe safe
OsString::from_vec— anOsStringthere is an arbitrary byte sequence, sothe conversion is total and no
unsafearises. Only Windows needsfrom_encoded_bytes_unchecked, because WTF-8 makes the conversion partial.That call rests on an invariant, now written where it can be checked: every sub-slice
the parser produces is cut at an ASCII byte — after
-, after--, at=, and betweenthe letters of a short-flag cluster — and an ASCII byte never occurs inside a multi-byte
sequence. The one way to break it is a non-ASCII
Flag::shortsentry, which would let acluster split mid-character. The derive already refused that; the rule is now documented
as load-bearing on both sides and has the test it was missing.
The crate goes from
forbid(unsafe_code)todeny, so the single audited exception hasto name itself rather than the crate pretending it has none. Parsing still contains no
unsafeat all.Byte-exactness is cheaper than the text path, not dearer: a
PathBuffield costs 567instructions per parse against a
Stringfield's 660, because it skips the UTF-8validation pass, and both allocate once. The gate fixture cannot show this — a spec
carries no Rust types, so every shadow field is a
String— so it is measured directly.A
Stringfield still reports rather than substitutes, which is the right answer for atype that cannot hold those bytes.
CI gains a Windows compile check, since every test here is
#[cfg(unix)]and nothing inthe pipeline was building the branch that carries the
unsafe.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Stack created with GitHub Stacks CLI • Give Feedback 💬
Note
Medium Risk
Touches argv-to-field conversion for path types and platform-specific behavior on Windows; Unix behavior is well-tested but the Windows path is compile-checked only, not exercised in tests.
Overview
PathBufandOsStringfields now keep argv bytes instead of requiring UTF-8, so paths like/tmp/\xffparse as the OS sees them rather than being rejected or mangled. The derive routes those types through newusage_argv::os_string_from_bytes: losslessOsString::from_vecon Unix, and on Windows a safe UTF-8 round-trip (invalid bytes becomeInvalidValue) rather thanfrom_encoded_bytes_unchecked, so the crate staysforbid(unsafe_code).Docs tie ASCII-only short flags to parser slicing (clusters and
=cuts), with derive validation and a test reinforcing that. Unix conformance tests cover detached, attached, short, repeated, andOsStringforms;Stringstill errors on non-UTF-8. CI adds ax86_64-pc-windows-msvccargo checkfor the non-Unix branch. PLAN.md marks the milestone done.Reviewed by Cursor Bugbot for commit 426f1bc. Bugbot is set up for automated code reviews on this repo. Configure here.
Cost
A
PathBuffield is now cheaper than the same field as aString, because it skips theUTF-8 validation pass:
Option<PathBuf>Option<String>The gate fixture cannot show this and was not used for it: a spec carries no Rust types, so
every field in the generated shadow is a
Stringand the shadow's numbers are unchanged(40,472 against 40,484 on the parent — the same code path, and the difference is noise).
Measured instead on a two-field probe, differencing
PARSE_N=0againstPARSE_N=100runs ofthe same binary and dividing.
Why this is sound
On Unix there is no
unsafe: anOsStringis an arbitrary byte sequence, so theconversion is the safe
OsString::from_vecand no invariant is required. This is worthstating plainly because it means the approval was only needed for one platform.
On Windows the encoding is WTF-8, not every byte sequence is valid, and only
from_encoded_bytes_uncheckedwill take one. That rests on an invariant of the crate:A token is split after
-, after--, at=, and between the letters of a short-flagcluster. All four are ASCII, and an ASCII byte never occurs inside a multi-byte WTF-8 or UTF-8
sequence, so no cut can land mid-character. Values passed through whole are trivially fine.
The one way to break it is a non-ASCII
Flag::shortsentry, which would let a cluster splitmid-character. The derive already rejected that — for ergonomic reasons — so the rule was
already true; what this PR adds is that it is now documented as load-bearing on both the
field and the check, and has the test it was missing.
Verification
--out <bad>), attached(
--out=<bad>, cut at the=), short (-o<bad>), short-with-equals (-o=<bad>),repeated into a
Vec<PathBuf>, and a bareOsStringfield. The cluster cases are the onesthe soundness argument leans on hardest, which is why they are enumerated rather than
sampled.
from_utf8_lossy, both byte-exactnesstests fail.
Option<Vec<PathBuf>>still tells "never given" from "given nothing" — that decision ismade before the conversion and had to survive the new path.
Stringfield still reports rather than substitutes, since it cannot hold those bytes.shortrejection now has a test, and theunsafebranch is compile-checkedfor
x86_64-pc-windows-msvc.One gap worth naming
Every test here is
#[cfg(unix)], and CI had no Windows job — so the branch carrying theunsafewas the one thing in the pipeline nothing built. This adds a Windows compile checkto
test.yml. A compile check is not coverage: the Windows conversion is still unexercisedby any test, and reaching it needs an unpaired surrogate in
argv. If that matters enough toyou, the next step would be a Windows test job rather than a check.
AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable.
Summary by CodeRabbit
New Features
Bug Fixes