Skip to content

fix(parse): enforce double_dash="required" for positional args - #762

Draft
JamBalaya56562 wants to merge 1 commit into
jdx:mainfrom
JamBalaya56562:fix-double-dash-required
Draft

fix(parse): enforce double_dash="required" for positional args#762
JamBalaya56562 wants to merge 1 commit into
jdx:mainfrom
JamBalaya56562:fix-double-dash-required

Conversation

@JamBalaya56562

@JamBalaya56562 JamBalaya56562 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Fixes the root cause behind jdx/mise#5759.

Problem

The parser never looked at SpecDoubleDashChoices::Required. Seeing a -- only set enable_flags = false; nothing consulted double_dash when assigning positionals. So for

arg "[file]" double_dash="required"

both of these succeeded:

mise run echo test        # should be a parse error
mise run echo -- test     # correct

The reverse half was missing too: an arg requiring -- that sits behind a greedy variadic was never reachable, even with a separator. In examples/mise.usage.kdl that is [-- TASK_ARGS_LAST]..., [-- ARGS_LAST]... and exec's [-- COMMAND]... — all declared, none ever filled.

Fix

double_dash="required" is generated from clap's Arg::last(true) (lib/src/spec/arg.rs, is_last_set() → Required), so both halves of that semantic are now implemented:

  • Rejection — a word offered to such an arg before an explicit -- is reported as ArgRequiresDoubleDash and not assigned. A variadic arg reports this once rather than once per word, and an arg that is both required and double_dash="required" no longer also collects MissingArg. The check runs ahead of validate_choices so a discarded word is not additionally reported as an invalid choice.
  • Routing — an explicit -- moves the positional cursor onto the arg that requires it, past earlier args including a greedy variadic.

Routing can leave an earlier arg empty, so "next arg" and "how many args are filled" no longer coincide. The cursor became an explicit index, and the two loops that used skip(out.args.len()) to find unfilled args — the MissingArg check and the default/env application in Parser::parse — now ask out.args by key instead of counting. Without that, a skipped arg would silently lose its default and its missing-arg diagnostic.

Interactions, both covered by tests:

  • A -- that double_dash="preserve" keeps as a value stays a value. It does not count as a separator and does not unlock a required arg — one token cannot be both.
  • restart_token resets the separator, so each invocation after it needs its own --.

Specs with no double_dash="required" arg are bit-for-bit unaffected: the cursor jump looks for such an arg and finds none.

Completions

ParseOutput gains next_arg (where the cursor stopped) and double_dash_seen. complete-word's local next_arg_for_completion re-derived the next arg from args and knew nothing about --, so with routing in place it would have disagreed with the parser; it now reads the parser's own cursor. While an arg is still locked behind a separator, the completion offers -- itself rather than values the parser would reject.

⚠️ These are new public fields on a struct that is not #[non_exhaustive], so this is technically a breaking API change. The only construction site is inside parse.rs. Happy to add #[non_exhaustive] in the same commit if you'd prefer to close that off now — say the word.

Behavior changes

Measured against examples/mise.usage.kdl:

Spec site Preceding arg Change
tasks add <TASK> + [-- RUN]... <TASK> (non-var) mise tasks add t echo hi is now an error — clap already rejects it, so this closes a divergence
root [-- TASK_ARGS_LAST]... [TASK_ARGS]... (greedy) post--- values move from TASK_ARGS to TASK_ARGS_LAST
tasks run [-- ARGS_LAST]... [ARGS]... (greedy) post--- values move from ARGS to ARGS_LAST
exec [-- COMMAND]... [TOOL@VERSION]... (greedy) post--- values move from TOOL@VERSION to COMMAND

The last three are the args finally doing what they were declared to do, but they change which usage_* variable a consumer reads. @jdx — worth confirming this is what mise expects before release, since mise is the main consumer of these specs. Spec authors who want the old permissiveness can drop back to double_dash="optional" (the default).

double_dash="automatic" remains unimplemented in the parser; this PR does not change that, and docs/spec/reference/arg.md now says so explicitly.

Tests

29 new tests, no existing test modified:

  • lib/tests/parse.rs — 17 end-to-end cases over the three spellings (double_dash="required", arg "[-- x]", arg "<-- x>"), covering rejection, routing past a greedy variadic, single-error guarantees, choices non-interference, and optional staying untouched.
  • lib/src/parse.rs — 10 unit tests asserting ParseOutput internals: error counts and kinds, the gap left by routing (defaults and MissingArg still correct), restart_token in both directions, preserve non-interference, and parse_partial staying Ok where parse errors.
  • cli/tests/complete_word.rs — 2 tests on a new choices-only fixture examples/double-dash.usage.kdl (no mount, no external completer, so it runs on every platform).

cargo test -p usage-lib --all-features is green (313 + 52 + 5). cargo fmt and cargo clippy are clean on the touched targets. I could not run the full CLI suite locally — the .sh-fixture and /dev/stdout tests fail on Windows for reasons unrelated to this change — so CI is the real check there.


This pull request was generated by Claude Code.

Summary by CodeRabbit

  • New Features

    • Added support for arguments available only after an explicit -- separator.
    • Values entered after -- are routed correctly, including with variadic arguments, restart tokens, and default subcommands.
    • Added clear error messages when the required separator is missing.
  • Bug Fixes

    • Improved command-line completion to suggest -- when required and resume relevant suggestions afterward.
  • Documentation

    • Expanded guidance and examples covering required separators and parsing behavior.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JamBalaya56562, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 61d11170-46cc-4b30-a251-33f3689e2119

📥 Commits

Reviewing files that changed from the base of the PR and between f4f00c7 and 22273ee.

📒 Files selected for processing (9)
  • cli/src/cli/complete_word.rs
  • cli/tests/complete_word.rs
  • docs/spec/reference/arg.md
  • examples/double-dash-default-subcommand.usage.kdl
  • examples/double-dash.usage.kdl
  • lib/src/error.rs
  • lib/src/lib.rs
  • lib/src/parse.rs
  • lib/tests/parse.rs
📝 Walkthrough

Walkthrough

The parser routes values to arguments that require --, exposes parser state for completion, reports separator-specific errors, and supports restart and preservation semantics. Completion, fixtures, documentation, and tests cover the behavior.

Changes

Required double-dash argument support

Layer / File(s) Summary
Parser output and public contracts
lib/src/parse.rs, lib/src/error.rs, lib/src/lib.rs
ParseOutput exposes next_arg and double_dash_seen. The library exports SpecDoubleDashChoices and adds UsageErr::ArgRequiresDoubleDash.
Separator routing and validation
lib/src/parse.rs
Parsing tracks positional indexes, routes values after --, handles restart tokens and preserved separators, and suppresses duplicate errors.
Parser regression coverage
lib/src/parse.rs, lib/tests/parse.rs
Tests cover required separators, positional gaps, variadic arguments, restart behavior, preservation, error handling, and unchanged optional behavior.
Completion and usage integration
cli/src/cli/complete_word.rs, cli/tests/complete_word.rs, examples/double-dash*.usage.kdl, docs/spec/reference/arg.md
Completion offers -- before required separators and resumes argument completion afterward. Fixtures and documentation describe the syntax and parsing rules.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Parser
  participant ParseOutput
  CLI->>Parser: parse partial command line
  Parser->>ParseOutput: publish next_arg and double_dash_seen
  ParseOutput-->>CLI: return completion state
  CLI->>CLI: offer `--` or complete the positional argument
Loading

Possibly related PRs

  • jdx/usage#753: Introduced the completion logic that this change replaces and extends.

Suggested reviewers: jdx

Poem

I’m a rabbit guarding the dash,
One separator makes values pass.
The parser hops, completion sings,
Tests track routing and gaps in strings.
-- opens the proper way.
Nibble-tested and shipped today!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes enforcing required double-dash separators for positional arguments.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR enforces double_dash="required" positional semantics and aligns dynamic completion with the parser’s positional cursor.

  • Rejects values supplied to locked positional arguments before an explicit --.
  • Routes post-separator values past earlier positional arguments, including greedy variadics.
  • Applies missing-argument, environment, and default handling by argument identity when routing leaves gaps.
  • Exposes parser cursor and separator state to complete-word, with focused parser and completion coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains within the scope of the available follow-up review threads.

Important Files Changed

Filename Overview
lib/src/parse.rs Implements required-separator enforcement, positional cursor routing, gap-aware validation/default handling, and parser-state exposure with comprehensive focused tests.
cli/src/cli/complete_word.rs Uses parser-owned cursor and separator state to offer -- while a positional remains locked and resume normal value completion afterward.
lib/src/error.rs Adds the dedicated diagnostic emitted when a positional value is supplied before its required separator.
lib/tests/parse.rs Adds end-to-end coverage for rejection, separator routing, shorthand forms, greedy variadics, choices, and optional behavior.
cli/tests/complete_word.rs Covers separator suggestions and completion routing beyond a greedy variadic.
docs/spec/reference/arg.md Documents enforced required-separator behavior, shorthand forms, routing semantics, and interactions with preserve and restart-token modes.

Reviews (2): Last reviewed commit: "fix(parse): enforce double_dash="require..." | Re-trigger Greptile

@JamBalaya56562
JamBalaya56562 force-pushed the fix-double-dash-required branch from 7d82ea0 to bf9bb63 Compare August 1, 2026 08:51
@JamBalaya56562

Copy link
Copy Markdown
Contributor Author

lint:semver is red here, and I'd like a steer on how you want it handled rather than guessing.

cargo semver-checks check-release -p usage-lib reports two major findings, both of which are the API change I flagged in the description:

--- failure constructible_struct_adds_field ---
  field ParseOutput.next_arg          lib/src/parse.rs:261
  field ParseOutput.double_dash_seen  lib/src/parse.rs:267

--- failure enum_variant_added ---
  variant UsageErr:ArgRequiresDoubleDash  lib/src/error.rs:30

Why each one is there

UsageErr::ArgRequiresDoubleDash — the diagnostic for "this arg only accepts values after --". UsageErr has no free-form message variant, and the nearest existing ones (MissingArg, InvalidInput) would either misreport an optional arg as missing or render as Invalid usage config.

ParseOutput::next_arg / double_dash_seen — once -- routes values past a greedy variadic onto the arg that required it, complete-word's local next_arg_for_completion (which returns the first unfilled arg and knows nothing about --) no longer agrees with the parser. Exposing the parser's own cursor keeps them from diverging.

#[non_exhaustive] does not rescue either one: adding it is itself a major change (enum_marked_non_exhaustive / struct_marked_non_exhaustive).

The structural bit

tasks/release-plz bumps versions at release time from the conventional-commit history, so Cargo.toml sits at the released version during review and semver-checks compares 4.1.0 → 4.1.0 ("no change; assume patch"). As far as I can tell that means any genuinely-breaking change fails this gate unless the PR itself bumps the version — which felt like your call, not mine, so I stopped here.

Options, either of which I'm happy to push

  1. Take the break. I add the version bump to this PR (or you do it however release-plz prefers), and the API stays as designed.

  2. Make it non-breaking. Concretely:

    • Drop the new variant and report the violation with bail! instead, exactly like the existing unexpected word: {w} and invalid-choice paths. Cost: errors stop accumulating, and parse_partial returns Err on an already-invalid line, so completions go quiet there — same as unexpected word does today.
    • Drop the two fields and let complete-word re-derive the state from the words it already holds (detect --, then prefer the first unfilled double_dash="required" arg; otherwise skip such args and offer --). Cost: the parser and the completion become two sources of truth for the same rule.

    The parser fix itself — rejection before --, routing after it — is unaffected either way.

Option 2 is maybe 30 minutes of work if you'd rather not spend a major on this. Just say which and I'll update the branch.


This comment was generated by Claude Code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/src/parse.rs (1)

749-757: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting the no-rewind case for an earlier required arg.

The search finds the first unfilled double_dash="required" argument anywhere in out.cmd.args, but the jump applies only when target > next_arg_idx. If such an argument is declared before the current cursor and the cursor already moved past it, the -- does not route to it. The argument is then reported as MissingArg.

This shape is unusual, because double_dash="required" mirrors clap Arg::last(true), which is the final positional. The behavior is safe, but the reason for the > guard is not stated in the comment. Add a sentence that records this decision.

🤖 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 `@lib/src/parse.rs` around lines 749 - 757, Add a concise comment beside the
`target > next_arg_idx` guard explaining that required double-dash arguments
declared before the current cursor are intentionally not revisited, preserving
forward-only routing and allowing them to remain reported as `MissingArg`.
🤖 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 `@cli/src/cli/complete_word.rs`:
- Around line 146-158: Centralize positional completion in a helper that applies
the `parsed.next_arg` and `parsed.double_dash_seen` rules, then use it from the
flag branch, `after_restart_token` path, and default-subcommand fallback instead
of calling `complete_arg` directly. When `double_dash_seen` is true, do not
classify dash-prefixed tokens as flags; when the next argument requires a
separator and it has not been seen, offer only `--` for an empty token and
suppress value/file completion. Add regressions covering dash-prefixed values
after `--`, `restart_token`, and `default_subcommand`.

---

Nitpick comments:
In `@lib/src/parse.rs`:
- Around line 749-757: Add a concise comment beside the `target > next_arg_idx`
guard explaining that required double-dash arguments declared before the current
cursor are intentionally not revisited, preserving forward-only routing and
allowing them to remain reported as `MissingArg`.
🪄 Autofix (Beta)

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: e7144fcd-3818-4ed2-9de2-0952e680467a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b2fde7 and bf9bb63.

⛔ Files ignored due to path filters (1)
  • mise.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • cli/src/cli/complete_word.rs
  • cli/tests/complete_word.rs
  • docs/spec/reference/arg.md
  • examples/double-dash.usage.kdl
  • lib/src/error.rs
  • lib/src/lib.rs
  • lib/src/parse.rs
  • lib/tests/parse.rs

Comment thread cli/src/cli/complete_word.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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 `@cli/src/cli/complete_word.rs`:
- Around line 178-186: Preserve and propagate the constrained result returned by
complete_positional in this branch instead of discarding it. Update the
surrounding completion flow so a constrained default-subcommand argument that
matches no choices after -- remains constrained, preventing the fallback at the
later choices handling from offering file paths.
- Around line 122-124: Update the positional-token completion logic near
complete_path so dash-prefixed tokens after parsed.double_dash_seen still use
file completion when no explicit choices are configured. Keep flag completion
disabled after -- while removing the unconditional ctoken.starts_with('-')
exclusion from the positional file fallback.
🪄 Autofix (Beta)

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: 6829058e-75bf-4b83-b091-c19a9cbcf3ab

📥 Commits

Reviewing files that changed from the base of the PR and between bf9bb63 and f4f00c7.

📒 Files selected for processing (9)
  • cli/src/cli/complete_word.rs
  • cli/tests/complete_word.rs
  • docs/spec/reference/arg.md
  • examples/double-dash-default-subcommand.usage.kdl
  • examples/double-dash.usage.kdl
  • lib/src/error.rs
  • lib/src/lib.rs
  • lib/src/parse.rs
  • lib/tests/parse.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • lib/src/lib.rs
  • lib/tests/parse.rs
  • lib/src/error.rs
  • docs/spec/reference/arg.md
  • lib/src/parse.rs

Comment thread cli/src/cli/complete_word.rs
Comment thread cli/src/cli/complete_word.rs
@JamBalaya56562
JamBalaya56562 force-pushed the fix-double-dash-required branch from f4f00c7 to c5f8b7d Compare August 1, 2026 14:32
The parser never looked at `SpecDoubleDashChoices::Required`. Seeing a `--`
only disabled flag parsing, so an arg declared

    arg "[file]" double_dash="required"

was filled whether or not the separator was given, and an arg sitting behind a
greedy variadic was never filled at all — even after a `--`. Reported against
mise in jdx/mise#5759, where `mise run echo test` and `mise run echo -- test`
both succeed.

Both halves of clap's `Arg::last(true)` are now implemented, which is what
`double_dash="required"` is generated from:

- A word offered to such an arg before an explicit `--` is rejected with
  `ArgRequiresDoubleDash` and not assigned. A variadic arg reports this once
  rather than once per word, and an arg that is both `required` and
  `double_dash="required"` no longer also collects `MissingArg`.
- An explicit `--` moves the positional cursor onto the arg that requires it,
  past earlier args including a greedy variadic.

Routing can leave an earlier arg empty, so "next arg" and "how many args are
filled" no longer coincide. The cursor becomes an explicit index and the two
loops that used `skip(out.args.len())` to find unfilled args — the `MissingArg`
check and the default/env application in `Parser::parse` — now ask `out.args`
by key instead of counting.

A `--` that `double_dash="preserve"` keeps as a value stays a value: it does not
count as a separator and does not unlock a required arg. `restart_token` resets
the separator, so each invocation after it needs its own `--`.

`ParseOutput` gains `next_arg` and `double_dash_seen` so completions follow the
same cursor as the parser. Every path that completes a positional — the one at
the cursor, the first arg after a `restart_token`, and a default subcommand's —
goes through one helper, so the rule cannot be honoured in one and forgotten in
another. Past a `--` the completion also stops offering flags, since the parser
reads everything after it as a positional value.
@JamBalaya56562
JamBalaya56562 force-pushed the fix-double-dash-required branch from c5f8b7d to 22273ee Compare August 1, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant