arg_tokenizer: shared CLI-arg tokenizer + cross-ecosystem -- boundary/flag fixes - #3681
Open
KuSh wants to merge 67 commits into
Open
arg_tokenizer: shared CLI-arg tokenizer + cross-ecosystem -- boundary/flag fixes#3681KuSh wants to merge 67 commits into
KuSh wants to merge 67 commits into
Conversation
KuSh
force-pushed
the
feat/arg-tokenizer
branch
2 times, most recently
from
August 23, 2026 23:20
15efaf3 to
ed26781
Compare
3 tasks
aeppling
requested changes
Aug 26, 2026
Contributor
|
Hey @KuSh Thanks for working on this which is a pain for commands rewirtting. repo with a stash that includes an untracked file:rtk git stash show -u file f containing a -- b:rtk grep -e -- f </dev/null directory with 4 files containing foo:rtk rg -z foo </dev/null |
aeppling
reviewed
Aug 26, 2026
aeppling
reviewed
Aug 26, 2026
Contributor
|
Also we may want to document this system in respective README.md |
Every src/cmds/** filter that needs to know "does this flag consume the next token as its value, and where does -- end options" was reimplementing that question independently. git.rs alone racked up 9 one-off bugfix commits on its own LogArg/consumes_next_token_as_value/log_arg_tokens (hardcoded matches! lists edited piecemeal, -- handling gaps), and a codebase survey found the same shape of bug already hit or latent in search.rs, golangci_cmd.rs, and dotnet_cmd.rs. Adds src/core/arg_tokenizer.rs: a single tokenize() that classifies an already-restore_double_dash'd args slice into Long/Short/Positional/ DashDash tokens, linking each value token to the flag that consumes it and vice versa, with a caller-supplied `takes_value(kind, name)` predicate (each wrapped CLI's value-flag list stays exactly as data — only the token-walking around it is now shared). Digit-only short suffixes (git log/head/tail's `-N` shorthand) are kept as one token rather than decomposed into per-digit "flags", since no real CLI defines boolean digit flags. Migrates git.rs's run_log (LogArg/log_arg_tokens/consumes_next_token_as_value -> log_takes_value + tokenize) and run_checkout's four separate hand-rolled scanners (checkout_new_branch_arg/checkout_reset_branch_arg/ checkout_branch_arg/checkout_restored_count) onto one shared tokenization each, keeping their existing flag lists as the predicate body. No intended behavior change; arg_tokenizer's own unit tests encode a regression case for every prior git.rs bugfix commit, and git.rs's existing test suite (updated only where it asserted internal token shape, e.g. dash-free flag text) plus the real-process integration tests in tests/guard_integration_test.rs still pass unchanged. Follow-up (not in this commit): migrate search.rs, dotnet_cmd.rs (which has a latent restore_double_dash gap of its own), and golangci_cmd.rs onto the same tokenizer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
search.rs's VALUE_FLAGS_SHORT/VALUE_FLAGS_LONG/ClusterResult/parse_cluster
reimplemented the same flag/value/-- classification arg_tokenizer now
centralizes. Migrates extract_pattern_path onto arg_tokenizer::tokenize,
keeping the flag lists as data (now dash-free, matching Token::text) and
the -e/--regexp-routes-to-patterns special case as extract_pattern_path's
own business logic (not something tokenize needs to know about).
Reconstructing the exact flags: Vec<String> shape (grep/rg-facing, not
just internal) needed one thing tokenize()'s per-character Short model
didn't originally have: knowing whether "-r" and "-n" were typed as one
cluster ("-rn", reconstructed glued) or two separate args ("-r" "-n"),
since existing tests pin the glued form for one-arg clusters. Added
Token::source_index (which original args slot a token came from) to
close that gap generically -- every Short token from one cluster shares
a source_index, a consumed separate-token value always has its own.
Also added Token::value() (attached-or-linked) as a small shared
convenience, used here and to simplify git.rs's now-redundant
linked_value/flag_value helpers.
Note: this is a reconstruction-format detail, not a behavior change --
grep/rg parse "-rn" and "-r -n" identically via standard getopt
clustering, and rtk's own has_short_flag() already checks via substring
so it doesn't care which form it sees either.
No intended behavior change. parse_cluster/ClusterResult's own unit
tests are removed (that internal API no longer exists); the existing
extract_pattern_path tests already exercise the same short-cluster/
value-taking/-e behavior end-to-end and all still pass unchanged.
Follow-up (not in this commit): golangci_cmd.rs, and dotnet_cmd.rs
(which has a latent restore_double_dash gap of its own).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dotnet_cmd.rs never called args_utils::restore_double_dash despite Build/Test/Restore/Format all using trailing_var_arg = true in main.rs, the same clap-strips-`--` hazard git.rs/cargo_cmd.rs/search.rs already hit and fixed (issue rtk-ai#1215). Concretely: `rtk dotnet test -- FullyQualifiedName=MyFilter` lost its `--`, so inject_report_trx_into_args (VsTestBridge MTP mode) couldn't find one to reuse and appended a fresh `-- --report-trx` at the end instead — landing the user's MTP-runtime filter expression BEFORE the separator (misread as a dotnet-test-level arg) instead of after it, and stranding --report-trx with nothing following it. Added the missing restore_double_dash call to both entry points that feed `args` into `--`-sensitive logic (run_dotnet_with_binlog for build/test/restore, run_format). Regression test stubs `dotnet` on PATH (no dotnet SDK in this environment) to capture the real argv rtk would hand it; verified it fails without the fix and passes with it. Not migrated onto arg_tokenizer: investigated during this pass and dotnet's CLI grammar isn't POSIX/GNU-style — has_nologo_arg accepts single-dash multi-letter flags (`-nologo`, `/nologo`) as one atomic name rather than a short-flag cluster, and --logger:trx uses `:` as an attach separator, both incompatible with arg_tokenizer's current clustering model. A caller-selectable dialect (no clustering, `/` prefix, `:`-or-`=` attach) could support this later; not attempted here to keep this fix minimal and low-risk. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lag checks
dotnet's CLI grammar isn't POSIX/GNU: `-flag`, `--flag`, and `/flag` are
all one atomic flag name (no short-flag clustering, e.g. `-nologo` must
stay one token, not decompose into per-char short flags), and a value
can attach via `:` as well as `=` (`--logger:trx`). Added Dialect::{Posix,
Msbuild} to arg_tokenizer, threaded through a new tokenize_dialect();
tokenize() itself is now a thin Dialect::Posix wrapper, so git.rs/search.rs
are untouched.
Migrated dotnet_cmd.rs's flag/value helpers (has_nologo_arg,
has_trx_logger_arg, has_results_directory_arg, has_report_arg,
has_report_trx_arg, extract_report_arg, has_verify_no_changes_arg,
has_write_mode_override, extract_results_directory_arg) onto
tokenize_dialect(Msbuild), replacing ~9 hand-rolled peekable/prefix
scans with two small shared helpers (dotnet_has_flag/dotnet_flag_value).
Two things the migration had to account for, not just carry over:
- dotnet_cmd.rs's checks were never `--`-boundary aware (--report-trx is
meaningful on either side of `--` depending on TestRunnerMode, and the
original code just flat-scanned the whole arg list) -- unlike git/grep,
where `--` genuinely ends option parsing. tokenize_dialect always stops
classifying at `--` (correct for git.rs/search.rs), so dotnet_cmd.rs's
new with_dotnet_tokens() strips `--` out before tokenizing to reproduce
the original scan-everything behavior. Caught by
test_vstest_bridge_respects_existing_report_trx (would otherwise
double-inject --report-trx when the user already placed it after --).
- has_nologo_arg previously matched only "-nologo"/"/nologo" (not
"--nologo"), and has_results_directory_arg/has_report_arg/
has_verify_no_changes_arg/has_write_mode_override previously matched
only the "--" form -- both incidental gaps from scanning raw strings
per-flag rather than a unified prefix model. Since dotnet's actual
System.CommandLine parser treats -/--/ / as interchangeable prefixes
for every option, recognizing all three forms uniformly for every flag
here is a correctness improvement, not a behavior risk.
inject_report_trx_into_args is untouched: it's pure `--`-relative Vec
splicing, not flag/value classification, so it doesn't need tokenizing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-options Discovered this refining the previous commit: dotnet's `--` doesn't mean what git/POSIX `--` means. Git's `--` ends option parsing -- everything after is a literal positional/pathspec, never a flag again. dotnet's `--` is an argument-*forwarding* boundary: what follows is still real flags, just meant for a different receiving parser (the VSTest/MTP test host), which can and does share flag names with dotnet test's own CLI (e.g. --logger, --results-directory are forwarded VSTest-console options, and --report-trx is meaningful on either side of `--` depending on TestRunnerMode::MtpVsTestBridge). The previous commit's dotnet_cmd.rs fix (with_dotnet_tokens stripping `--` out before tokenizing) reproduced the right observable behavior but for the wrong reason -- it papered over the mismatch in the caller instead of naming it. Moved the fix to where it actually belongs: tokenize_dialect now only stops classifying at `--` for Dialect::Posix; Dialect::Msbuild still emits a DashDash token (so callers can find `--`'s position) but keeps classifying flags normally past it. This also let dotnet_cmd.rs drop the owned-copy-plus-closure workaround entirely -- dotnet_tokens() is back to a plain function borrowing straight from the caller's args, same shape as every other tokenizer call site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…okenizer find_subcommand_index/split_flag_name/golangci_flag_takes_separate_value hand-rolled the same flag/value classification arg_tokenizer now centralizes (GLOBAL_FLAGS_WITH_VALUE was its own one-off "which global flags take a separate value" list). Replaced with a single arg_tokenizer::tokenize(Dialect::Posix) call plus a small predicate (golangci_takes_value); find_subcommand_index now just walks the resulting tokens for the first free positional, using Token::source_index to translate back to the position in the original `args` for the caller (classify_invocation slices global_args/run_args off that index). No intended behavior change: all existing classify_invocation tests pass unchanged. Added two regression cases the prior hand-rolled scanner didn't have tests for: `-c value` (short flag consuming a separate-token value, previously untested) and `--` before any subcommand is found (must fall through to Passthrough, matching golangci-lint's own parser taking over from there). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…catch-alls
Consistency sweep across everything touched in this arg_tokenizer work:
- `TokenKind::X | TokenKind::Y => false` catch-alls (naming exactly the
two variants not otherwise handled) become `_ => false`/`_ => {}`.
- match arms on TokenKind reordered alphabetically (Long before Short,
etc.), matching the convention already used by most of them.
- matches!() flag lists reordered alphabetically where they weren't
(dotnet_takes_value's report/results-directory, run_log's format-flag
and wants_merges checks, parse_limit_from_tokens' arm order).
- search.rs's VALUE_FLAGS_SHORT/VALUE_FLAGS_LONG and golangci_cmd.rs's
GOLANGCI_SUBCOMMANDS (&[&str] consts checked via .contains()) converted
to matches!() functions (is_short_value_flag/is_long_value_flag/
is_golangci_subcommand), consistent with every other flag-list
predicate in this codebase.
- TokenKind and Dialect enum variants reordered alphabetically at their
declaration site too (DashDash/Long/Positional/Short,
Msbuild/Posix).
No behavior change; cargo fmt/clippy/test --all clean throughout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…property dotnet_cmd.rs's dotnet_flag_value/dotnet_has_flag each hardcoded their own .eq_ignore_ascii_case() call. Case-insensitive flag names aren't a dotnet-CLI particularity though -- it's a broader Windows/MSBuild-ecosystem convention (classic MSBuild.exe's /nologo and /NoLogo are equally valid, not just dotnet's System.CommandLine parser), so it belongs on Dialect itself, not left to each caller to remember. Token::text can't be case-folded once at tokenize time without giving up the zero-copy &'a str contract (there's no borrowed "lowercased" view of a str), so the fix lives in new shared lookup helpers instead: flag_value(tokens, dialect, name) / has_flag(tokens, dialect, name), which pick exact vs ASCII-case-insensitive comparison from the dialect via a small flag_name_matches() (Long tokens only -- Msbuild never produces Short, and conflating Long/Short by text alone would be wrong for Posix). dotnet_cmd.rs's local helpers now just forward to these. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run_log's own comment said "tokenize once and share it," but requests_raw_log_output(args) tokenized internally to decide on raw passthrough, and when it returned false the code tokenized args again right after for the shared `tokens` variable -- silently contradicting its own stated intent. Found by /code-review high. run_log now tokenizes once up front and checks tokens.iter().any(requests_raw_diff_shape) directly for the raw-passthrough decision, reusing the same tokens for everything else. requests_raw_log_output stays as a #[cfg(test)] convenience wrapper (its existing unit tests still call it directly with a &[String]) but is no longer part of the real run_log path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- dotnet_cmd.rs: has_trx_logger_arg only checked the first --logger occurrence via flag_value's .find(). dotnet test's --logger can legitimately repeat (`--logger "console;verbosity=normal" --logger trx`, a documented VSTest pattern); a trx logger that isn't the first occurrence was silently missed, causing a duplicate --logger trx injection. Added arg_tokenizer::flag_values (every occurrence, not just the first) and switched has_trx_logger_arg to it. Verified the new multi-logger regression test fails without the fix. - arg_tokenizer.rs: TokenKind::DashDash's doc promised "emitted exactly once," but under Dialect::Msbuild seen_dash_dash was never set, so every literal `--` produced its own DashDash token (Posix already got this for free via its seen_dash_dash-gated positional catch-all). Added a separate emitted_dash_dash flag so only the first `--` becomes DashDash in either dialect; later ones fall back to plain Positional text "--". Latent today (no dotnet_cmd.rs caller reads DashDash position yet) but was a real violation of the module's own invariant. - golangci_cmd.rs: find_subcommand_index stopped scanning (returning None/Passthrough) the moment it hit a bare "-" token, since arg_tokenizer classifies a lone "-" as Positional. The old hand-rolled scanner treated any "-"-prefixed token, including bare "-", as skippable/unrecognized and kept scanning for the real subcommand. Excluded "-" specifically from the stopping condition. Verified the new regression test fails without the fix. - search.rs: extract_pattern_path cloned every arg into a fresh Vec<String> to satisfy arg_tokenizer::tokenize's &[String] signature, even though its one production caller already owns a Vec<String> from restore_double_dash. Split into extract_pattern_path_owned(&[String]) (used by the real call site, zero-copy) and kept the generic T: AsRef<str> wrapper as #[cfg(test)]-only for the ~29 existing test call sites that pass &[&str] literals. Also assessed and intentionally left unfixed (see conversation for detail): run_show/run_diff's own --stat-style checks have the same `--`-boundary gap run_log had, but touching them is new scope beyond this session's migrated code, not a regression this diff introduced; dotnet_cmd.rs's flag-prefix widening (-/--// treated as interchangeable for every dotnet_cmd.rs flag) rests on an unverified assumption about dotnet's System.CommandLine parser I can't confirm without a real SDK; a few remaining hand-rolled string scans (has_binlog_arg, has_verbosity_arg, has_output_flag) are consistency nitpicks, not bugs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tokenize/tokenize_dialect were hardcoded to &[String], forcing search.rs's extract_pattern_path to clone every arg into a fresh Vec<String> just to satisfy that signature even for its one production caller, which already owns a Vec<String> from restore_double_dash (worked around last commit by splitting into a test-only generic wrapper + an _owned zero-copy variant). Genericizing tokenize/tokenize_dialect/push_atomic_flag over T: AsRef<str> removes the need for that split entirely: String and &str both satisfy the bound, so every existing call site (all of which pass &[String]) needed zero changes, and search.rs collapses back to one extract_pattern_path<T: AsRef<str>> used directly by both its production call site and its ~29 &[&str]-literal tests, with no cloning either way. Not extended to OsStr/OsString: unlike str, OsStr exposes almost no string-manipulation API by design (no strip_prefix, split_once, char-boundary slicing), so tokenizing it would mean re-deriving that machinery byte-by-byte the way clap_lex does internally -- a much bigger change for a case rtk doesn't hit today (its own CLI parsing already assumes UTF-8 args for every subcommand this module serves). Also documents why tokenize() doesn't just call restore_double_dash (or std::env::args()) internally: restore_double_dash's result has to be an owned Vec<String> the caller holds in its own `let`, and Token<'a> borrows straight from `args` -- tokenizing a Vec<String> built inside this module would tie every Token to a value dropped when the function returns. Same root cause as why this module doesn't build on clap_lex. Also: dotnet_cmd.rs's has_trx_logger_arg no longer needs an intermediate `let has_trx = ...; has_trx` binding to dodge a tail-position temporary lifetime issue -- an explicit `return` avoids it more simply. And added a regression test + doc trail for the earlier -5x malformed-input question: verified against real git (`git log -5x` fails with "fatal: '5x': not an integer", exit 128) that run_log's internal limit computation for this input is never observable either way, since run_log bails out on !result.success() before the formatting code that would use it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dotnet_tokens(args) re-tokenized from scratch inside every has_*/extract_* helper (has_nologo_arg, has_trx_logger_arg, has_results_directory_arg, has_report_arg, has_report_trx_arg, extract_report_arg, has_verify_no_changes_arg, has_write_mode_override, extract_results_directory_arg, has_binlog_arg, has_verbosity_arg) -- unlike git.rs's run_log, which already tokenizes once and shares the result. A single `dotnet test`/`dotnet format` invocation could re-tokenize the same args up to 5+ times across run_format's own checks plus build_effective_dotnet_format_args, or run_dotnet_with_binlog's should_expect_binlog plus resolve_trx_results_dir plus build_effective_dotnet_args's several checks. Every one of these helpers now takes `tokens: &[Token<'_>]` directly instead of `args: &[String]`, with the tokenization itself hoisted to a single `dotnet_tokens(args)` call at the top of run_format and run_dotnet_with_binlog, threaded down through resolve_trx_results_dir, resolve_format_report_path, build_effective_dotnet_args, and build_effective_dotnet_format_args. `args` itself stays a parameter alongside `tokens` where still needed for non-classification purposes (raw forwarding to the dotnet subprocess, detect_test_runner_mode's filename-extension scan, inject_report_trx_into_args's positional Vec-splice). Also migrated has_binlog_arg/has_verbosity_arg (previously hand-rolled starts_with string scans) onto the tokenizer as part of this pass, since they were two of the remaining "cleanup opportunity" holdouts sitting a few lines above the already-migrated helpers. No intended behavior change; full suite (2731 tests, including the dotnet_double_dash_test.rs real-process regression test) passes unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
has_output_flag was the last hand-rolled string scan left in golangci_cmd.rs (find_subcommand_index/golangci_takes_value were already migrated), and had no `--` awareness: `golangci-lint run -- --out-format` (a package path literally named "--out-format" passed to the linted target after the separator) would be misdetected as the user requesting a custom output format, silently skipping RTK's own default --output.json.path/--out-format=json injection. Migrated onto arg_tokenizer::tokenize + has_flag, consistent with the rest of the file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…/ interchangeable
Verified against a real dotnet 9 SDK (Docker, mcr.microsoft.com/dotnet/sdk:9.0)
that the earlier claim behind the Msbuild dialect's uniform prefix widening
("dotnet's actual System.CommandLine parser treats -/--// as interchangeable
prefixes for every option") was wrong. Only a handful of options are genuine
legacy MSBuild.exe passthrough switches valid via all three prefixes
(-nologo/--nologo/nologo, -bl, -v/-verbosity) -- confirmed working. Every
other dotnet_cmd.rs flag (--verify-no-changes, --report for `dotnet format`;
--logger, --results-directory for `dotnet test`) is double-dash-only, and a
single-dash or slash spelling doesn't just get rejected -- it gets
*misparsed* as an unrelated MSBuild switch:
$ dotnet format -verify-no-changes
Argument 'erify-no-changes' not recognized. Must be one of: 'q' 'quiet' ...
(parsed as -v with attached value "erify-no-changes")
$ dotnet test -results-directory /tmp/out
MSBUILD : error MSB1001: Unknown switch.
$ dotnet test -logger trx
MSBUILD : error MSB1007: Specify a logger.
(collides with MSBuild's own -logger switch, which wants an assembly spec)
$ dotnet format /verify-no-changes
Unhandled exception: FileNotFoundException: 'verify-no-changes' does not
appear to be a valid project or solution file.
(positional argument fallback, same as -write / --write's actual behavior)
Added Token::double_dash (true only for a literal `--` prefix; -flag//flag
under Dialect::Msbuild are false) plus has_double_dash_flag/
double_dash_flag_value/double_dash_flag_values, and switched
has_trx_logger_arg/has_results_directory_arg/has_report_arg/
has_report_trx_arg/extract_report_arg/has_verify_no_changes_arg/
extract_results_directory_arg/has_write_mode_override to the strict variants
-- has_nologo_arg/has_binlog_arg/has_verbosity_arg keep the loose has_flag,
since those three are the actual legacy-switch exceptions. --write is RTK's
own pseudo-flag (stripped before forwarding to real dotnet, never validated
by it) but kept double-dash-only too for consistency with its dotnet-format
siblings rather than inventing a laxer convention with no real reference
behavior to match.
Removed the now-unused loose flag_value/flag_values (nothing needs
loose-prefix *value* extraction, only loose presence checks for the three
legacy switches) and dotnet_cmd.rs's now-dead dotnet_flag_value wrapper.
Regression test (test_double_dash_only_flags_reject_single_dash_and_slash_spellings)
verified to fail against the prior (loose) implementation before restoring
the fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y -- Critical finding from the third /code-review high pass, independently verified against real git before fixing: both push_atomic_flag (Long/Msbuild-atomic flags) and the Short-cluster value-consumption branch consumed the next raw arg as a value-taking flag's value via args.get(...) unconditionally, with no check for the literal "--" separator. So `rtk git log --grep -- pattern` or `rtk git checkout -b -- file.txt` silently swallowed "--" as the flag's value and never emitted a DashDash token at all -- breaking anything downstream that depends on finding that boundary (e.g. git.rs's checkout_restored_count/checkout_branch_arg). Verified against real git 2.51 that this exact swallowing is wrong: $ git log --grep -- fix fatal: Option '--grep' requires a value (git refuses to let --grep claim "--", even though --grep needs a value) $ git log --grep -- -- foo fatal: Option '--grep' requires a value (still refuses -- only the FIRST/boundary "--" is protected) $ git log -- -- fix (succeeds -- once past the boundary, a later "--" is ordinary text and fair game as a value) So the fix is precise, not a blanket "never consume --": guard value consumption on the tokenizer's own emitted_dash_dash state (already tracked for the Msbuild-forwarding-boundary fix) -- refuse to link the still-unseen boundary "--" as a value, but still allow a later "--" once the boundary was already emitted. Threaded emitted_dash_dash into push_atomic_flag as a parameter; the short-cluster branch already had it in scope. Also extracted the shared link_next_value() helper both consumption sites now call, addressing a related cleanup finding from the same review: the duplicated consume-next-token-as-value logic is exactly what let the original bug exist in two places at once instead of one. Three new regression tests (verified to fail against the prior implementation before restoring the fix): a value-taking Long flag before --, a value-taking Short flag before --, and a value-taking flag after the boundary was already emitted (which must still consume normally). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- dotnet_cmd.rs: has_write_mode_override matched an attached-value
spelling ("--write=true") as a force-write override via
dotnet_has_double_dash_flag (which only checks the flag name, not
whether it has a value), but build_effective_dotnet_format_args's
strip filter only ever removed the exact bare "--write" token. So RTK
would skip its own --verify-no-changes injection *and* still forward
the unrecognized "--write=true" to real dotnet, which has no --write
option at all (confirmed via Docker) and would reject it. --write was
never meant to take a value, so detection now requires attached ==
None too -- an attached-value spelling is simply not recognized as
the --write pseudo-flag, matching how any other unrecognized argument
passes through untouched (consistent with, not silently swallowed by,
the strip filter).
- golangci_cmd.rs: run_filtered called build_filtered_args (which
tokenizes invocation.run_args) twice -- once to build the real
command, once again just to render the verbose debug log line.
Compute it once and reuse it for both.
Investigated but found NOT to be real bugs (verified against a real
dotnet 9 SDK via Docker before concluding, rather than assuming):
- arg_tokenizer.rs's split_attached splitting on both ':' and '=' for
every Dialect::Msbuild Long token, including double-dash-only options
like --results-directory/--report: confirmed `--results-directory:val`
and `--report:val` both work correctly against real dotnet test/format,
so the existing behavior is correct, not over-permissive.
Not changed, judged intentional/established rather than problems:
dotnet_cmd.rs's per-flag wrapper functions (has_nologo_arg etc.) --
named predicates over hardcoded string literals, matching the
pre-existing convention from before this session's migration; Token::
double_dash being meaningful only under Dialect::Msbuild -- normal for
a field on a struct shared across dialects with different needs;
git.rs's #[cfg(test)]-only wrappers (real_flag_args, parse_user_limit,
requests_raw_log_output) -- a deliberate tradeoff from an earlier commit
in this branch to keep production on the single-tokenization path
without rewriting ~15 existing test call sites.
Also noted but out of scope for this branch (per user instruction):
run_show/run_diff's own `--`-boundary gap, already tracked as a known
follow-up since commit 7049983.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run() never called restore_double_dash despite clap's trailing_var_arg stripping the literal "--", so both classify_invocation's -- detection and the raw args forwarded to the real binary were missing the user's own --. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tokenizer
run_add never called restore_double_dash, so `rtk git add -- -weird-filename`
lost its -- before reaching git, which rejected the flag-like filename
instead of staging it.
run_show's wants_stat_only/wants_format checks were raw, --boundary-unaware
string scans (arg == "--stat", arg.starts_with("--pretty")) -- a pathspec
literally named "--stat" or "--pretty" after -- was misdetected as the real
flag. Migrated onto arg_tokenizer (sharing git log's value-taking-flag
predicate) with restore_double_dash up front, matching the fix already
applied to run_log/run_diff/run_checkout. wants_blob_show/is_blob_show_arg
is left as-is: its own dash-prefix guard already makes it boundary-safe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the existing git_log_dash_p_pathspec_after_double_dash test: stages a file literally named "--stat" past the -- separator and asserts `rtk git show -- --stat` stays on RTK's compacted-diff path instead of being misdetected as the --stat summary flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-nologo is a pure boolean MSBuild.exe switch (confirmed via a real dotnet 9 SDK, Docker: "-nologo:true" fails with "MSB1002: This switch does not take any parameters"). has_nologo_arg used the loose dotnet_has_flag lookup, which ignores Token::attached entirely, so it treated the broken "-nologo:true" spelling as "user already passed -nologo" -- RTK skipped its own -nologo injection while still forwarding the invalid flag straight through to real dotnet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run_branch never called restore_double_dash despite git branch using
trailing_var_arg, and has_action_flag/has_list_flag/has_positional_arg
were raw, --boundary-unaware string scans (has_positional_arg in
particular: `!a.starts_with('-')`). A branch name starting with '-'
after -- was misclassified as a flag rather than the positional name to
create, so rtk silently ran a harmless empty `git branch -a` list
instead of attempting the creation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eport_trx_into_args inject_report_trx_into_args hand-rolled its own args.iter().position(|a| a == "--") scan instead of reusing the DashDash token already computed by dotnet_tokens for this same invocation. Pure refactor, no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
is_short_value_flag treated -T as value-taking for both grep and rg, but confirmed via real grep --help/rg --help that -T means unrelated things per engine: GNU grep's -T is --initial-tab (boolean, no value), ripgrep's -T is --type-not (takes a value). `rtk grep -T pattern file.txt` consumed "pattern" as -T's (nonexistent) value, leaving "file.txt" misread as the search pattern and dropping the real pattern entirely. Threaded Engine through is_short_value_flag/search_takes_value, matching the pattern already used for is_format_flag_token's -L/-z fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…value has_limit_flag matched any Short "n" token by text alone, even one clustered with another short flag where log_takes_separate_value's is_solo gate correctly refuses to link a value (e.g. "-cn 2" leaves "n" with no attached/linked value at all, per the earlier clustering fix). RTK would then believe the user explicitly set a commit-count limit (skipping its own --no-merges default) even though no value was ever captured. Extracted has_limit_flag into its own testable function (matching parse_limit_from_tokens's convention) and required t.value() to be Some for the "n" case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
is_short_value_flag treated -r as unconditionally boolean, but confirmed via real rg (`rg -rn hello file` prints "n", `rg -rXXX hello file` prints "XXX" -- proof -r consumes the cluster remainder as its --replace value) that ripgrep's -r takes a value (grep's -r/-R is --recursive, boolean, with no equivalent to rg's -r at all). `rtk rg -rREPLACEMENT pattern src` was misread as a boolean cluster instead of -r's attached replacement value, corrupting the actual command RTK executes -- extract_pattern_path's reconstructed patterns/paths/flags feed directly into building the invoked command, not just filtering, so this was a real command-substitution bug. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run_stash's "show" branch used a raw arg == "-p" scan for patch-mode detection, the same bug class already fixed for sibling run_log/ run_diff/run_show -- a pathspec literally named "-p" or "--patch" after `--` was wrongly treated as the real patch flag, routing to compact_diff instead of RTK's compact_stash_stat summary. Extracted into a testable stash_show_wants_patch(args) function using tokenize_git + log_takes_value/log_takes_separate_value, matching only the patch-mode flags (not the full requests_raw_diff_shape list, since --stat et al. correctly stay on the compact_stash_stat branch here). An end-to-end integration test proved impractical: real git's own `git stash show -- <pathspec>` unconditionally shows full patch content whenever limited by any pathspec, regardless of -p, so the final git output can't distinguish flag-vs-pathspec interpretation -- verified this with a direct git session before settling on a unit-level test against the extracted predicate instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng flags dotnet_takes_value's allowlist (logger/report/results-directory) was narrower than dotnet's real value-taking flags. Confirmed via a real dotnet 9 SDK (Docker, dotnet test --help/dotnet build --help) and added --filter, -c/--configuration, -f/--framework, -r/--runtime, -a/--arch, --os. Without these, e.g. `dotnet test --filter FullyQualifiedName~Foo.csproj` tokenized the filter value as an unlinked Positional, misread by detect_test_runner_mode_in_dir's explicit_projects filter as an explicit project reference (since it coincidentally ends in .csproj), skipping the working-directory scan. The short forms (-c/-f/-r/-a) are matched under TokenKind::Long, not Short: under Dialect::Msbuild, tokenize_dialect routes every single-dash arg through push_atomic_flag, which always produces a Long token (Msbuild has no character-clustering concept the way Dialect::Posix does) -- also confirmed dotnet has no glued short form like -cRelease either way (MSB1001: Unknown switch; only "-c Release" or "-c=Release" work). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
golangci_run_takes_value's allowlist omitted "out-format" even though has_output_flag explicitly checks for it -- its separate-token value (e.g. `--out-format json`) tokenized independently as an unlinked Positional instead of being linked, the same bug class already fixed for --path-prefix and other run-level flags in this same file. out-format is a v1-only legacy flag (golangci-lint 2.x's --help no longer lists it), kept for v1 installs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…en_dash_dash - has_double_dash_flag/double_dash_flag_value/double_dash_flag_values shared the same 3-part match condition inline three times; factored into is_double_dash_flag(). double_dash_flag_value keeps its own find() (not double_dash_flag_values().next()) since that would subtly change semantics: "first match regardless of whether it has a value" vs "first match that has a value". - seen_dash_dash was always exactly emitted_dash_dash && dialect == Posix (only ever set true immediately after emitted_dash_dash inside the Posix branch); removed the redundant variable and derived the condition directly. Pure refactor, no behavior change either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ll sites "Positional kind, and not consumed as some preceding flag's value" was hand-spelled identically in git.rs (x2), golangci_cmd.rs, search.rs, and dotnet_cmd.rs. Added Token::is_free_positional() to the shared type and switched all 5 call sites to it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…4 call sites Finding the `--` boundary token's position was hand-rolled independently at dotnet_cmd.rs:713,864 and git.rs:1328,1352. Added arg_tokenizer::dashdash_index (index into the tokens slice; tokens[i].source_index recovers the original-args position) and has_dashdash (presence check), and switched all 4 call sites to them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l sites log_takes_value/log_takes_separate_value were passed to tokenize_git at 9 separate call sites across run_log/run_diff/run_show/run_stash and their test helpers. Added git_log_tokens(args), mirroring dotnet_cmd.rs's dotnet_tokens() consolidation, and switched every call site to it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red predicate Both hand-rolled the identical "Long flag, no attached value, case-insensitive name match" shape (differing only in whether double_dash is required), because neither arg_tokenizer.rs primitive checks `attached` at all. Factored into dotnet_has_bare_loose_flag/ dotnet_has_bare_double_dash_flag, matching the existing dotnet_has_loose_flag/dotnet_has_flag naming split. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…value golangci_run_takes_value hand-duplicated golangci_takes_value's 5 flag names (color/config/cpu-profile-path/mem-profile-path/trace-path) instead of building on it, even though golangci_takes_value's set is a strict subset (every pre-run flag is also valid after run). That duplication is exactly how the "out-format" gap (fixed earlier this branch) came about -- one list got a new entry, the other didn't. Pure refactor, behavior-preserving (confirmed by the full existing test suite, unchanged). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…canner struct push_atomic_flag/link_next_value threaded 7-8 positional parameters (tokens, args, i, dialect, takes_value, takes_separate_value, emitted_dash_dash) by hand, carrying #[allow(clippy::too_many_arguments)]. Introduced a Scanner<'a, 'p, T> struct grouping this shared scan state, with push_atomic_flag/link_next_value as &mut self methods. tokenize_dialect_ex now owns one Scanner instance instead of five loose local variables threaded through free functions -- a future addition to the scan state (this module already had to add one, takes_separate_value) means one new field instead of a parameter threaded through every helper and call site. Pure refactor, no behavior change: full test suite (2762 tests) passes unchanged, clippy is clean with no allow needed anymore. Verified the one pre-existing --ignored test failure (test_git_status_not_a_repo_exits_nonzero) is an unrelated, pre-existing locale-dependent flake by reproducing it identically on the pre-refactor commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
explicit_projects has no filesystem backstop and relies entirely on dotnet_takes_value's allowlist being exhaustive; a real dotnet flag missing from it (e.g. --property/-p, --collect, --diag, --settings, --blame, --output/-o, --source, --sln) whose value ends in a project extension would still be misread as an explicit project path, the same class of bug three earlier commits in this branch each closed one flag at a time. Same root cause as arg_tokenizer.rs's Msbuild single-segment-path limitation (real MSBuild needs a stat() call to resolve the analogous ambiguity) -- documented as a known, deliberate tradeoff rather than fixed, since teaching this function to stat() every candidate would be a materially different, slower design. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onstructed strings has_short_flag scanned the reconstructed flags: Vec<String>, which also contains value-taking flags' own separate-token values as bare, unmarked strings alongside real flags -- a value starting with '-' and containing the queried letter was misread as the real flag (e.g. `rtk rg --replace '-Chart' pattern src` misdetected a -C context flag from --replace's own value). Added DetectedFlags (show_file/show_line/context), computed directly from token kind/linked in extract_pattern_path's own walk -- the same technique already used for has_format_flag, extended to cover has_context_flag/show_file/show_line's letters (H/r/R/n/N/A/B/C) too. A consumed value token is never examined as a candidate flag at all, regardless of its text, since it's classified Positional+linked rather than re-derived from a reconstructed string. show_file's -r/-R check is engine-aware (only counts for Grep: rg's -r is --replace, an unrelated value-taking flag, not recursive-descent; rg has no -R at all), matching the is_format_flag_token/ is_short_value_flag precedent already established for -L/-z/-T/-r elsewhere in this file. show_file/show_line/has_context_flag/has_short_flag become #[cfg(test)]-only wrappers preserving the existing extensive test suite unchanged; the now-fully-dead show_file (paths+flags combined) production wrapper is replaced by wants_show_file (the paths-only half, combined with DetectedFlags::show_file at each call site) since it had no direct test callers of its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
requests_raw_diff_shape's -p/-u/--patch match was correct for run_log (whose default output has no diff content at all, so an explicit -p is a genuine request for something the filtered path never produces) but was wrong when run_diff/run_show started sharing that same predicate this branch: diff/show's default output already IS patch text, so an explicit -p/-u/--patch is redundant with the default, not a request for an incompatible shape. Sharing the full list meant `git diff -p` / `git show -p <rev>` silently skipped RTK's stat + compacted-diff pipeline and returned full raw output instead -- a real token-savings regression for a very common invocation, caught by /code-review high with zero test coverage. Add requests_diff_show_raw_shape, the same shape-flag list minus patch/p/u, and point run_diff/run_show at it while leaving run_log on the original requests_raw_diff_shape. --patch-with-raw/--patch-with-stat still trigger the raw path for diff/show since they mix patch text with a raw/stat format RTK's own extra --stat step would double up against. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ction sites
The DashDash, digit-run Short, and cluster-loop Short token pushes each
hand-built a full Token{...} struct literal instead of reusing the
positional()-style constructor already used elsewhere in the same function
(and push_atomic_flag's Long push did the same). Token gaining a new field
later -- a growth pattern this module's own docs already anticipate, citing
takes_separate_value as precedent -- meant updating 4 literals instead of 1
call site, with a forgotten field at one of them silently defaulting wrong
instead of failing to compile.
Generalize positional() into token(kind, text, source_index, double_dash),
have positional() call it, and update every other construction site to build
on it via struct-update syntax the same way link_next_value already does on
top of positional(). Pure refactor, no behavior change: cargo test --all
passes the same 2765 tests before and after.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dash_test.rs The test itself is #[cfg(unix)] (it spawns a /bin/sh stub using unix file permissions), but its Command import and shell_quote helper weren't gated the same way -- on Windows the test compiled out entirely, leaving both genuinely unused and tripping -D unused-imports / -D dead-code. Nest both inside the test function's body instead of gating them separately: since the whole function is already #[cfg(unix)], that one attribute now covers everything, and Linux/macOS behavior is unaffected (cargo test --all still passes the same 2765 tests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e_value tokenize_git had exactly one caller in the whole codebase (git.rs's own git_log_tokens()), and zero git-specific code -- it's a thin wrapper fixing Dialect::Posix while leaving takes_separate_value customizable, reusable by any POSIX-ish tool with the same short-flag clustering quirk. Naming a shared, mechanism-generic function after its only current caller was misleading: either it should generalize (drop the git-specific name) or it should live in git-specific code. Since the git-specific rationale (the -n/-M-family rules) was already documented at git.rs's own log_takes_separate_value, not lost by renaming, generalizing was the smaller change. Trimmed the moved function's doc comment down to the general mechanism, pointing to log_takes_separate_value for the git-specific quirks it originally listed inline. Pure rename, no behavior change: cargo test --all passes the same 2765 tests before and after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--report-trx is a pure boolean flag, same class as --write/--nologo (already fixed earlier in this branch). The non-bare dotnet_has_flag lookup let a broken spelling like --report-trx:true count as "already present," skipping RTK's own injection while still forwarding the unrecognized flag straight through to real dotnet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…diff_shape Was duplicating requests_raw_diff_shape's Long-flag list verbatim (minus "patch") instead of delegating and excluding the patch-only cases. Same list-drift pattern that already caused the git diff -p/ git show -p regression fixed by 7533879 -- delegating rules out the two lists silently diverging again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y option's value stash_show_wants_patch reused git log's separate-value grammar (log_takes_value/log_takes_separate_value), under which --author is a value-taking Long flag. git stash show's real grammar is much narrower ([-u] <diff-options> [<stash>]) and doesn't recognize --author at all -- confirmed against real git 2.53.0 that `git stash show --author -p` produces full patch output, since --author consumes no value there. The old code swallowed the following -p as --author's value, causing patch content to render through the stat-only compact_stash_stat path instead of compact_diff. Now tokenizes with a predicate that never treats any flag as taking a separate value, since this detector only needs to spot a bare -p/-u/--patch token -- that can only ever false-positive, never silently swallow a real one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-u means --include-untracked for git stash show, not -p (unlike git log, where -u is a -p synonym). Conflating them routed -u's stat-only output through compact_diff, which only renders patch content, producing silently empty output where a stat summary was expected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ir value grep -e -- f (and any other value-taking grep/rg flag followed by a literal --) was misparsed: the tokenizer's -- boundary guard is correct for git/cargo, which always reject -- as a pending value, but grep/rg both consume -- unconditionally as any value-taking flag's value (confirmed for -A/-m/-e/--context/--file across both engines). RTK treated -- as the end-of-options boundary instead, so `rtk grep -e -- f` searched for pattern "f" in /dev/null instead of "--" in file "f". Adds Scanner::claims_literal_dash_dash, exposed as an opt-in field on a new TokenizeOptions passed to tokenize_with_options -- defaults to false everywhere except search.rs, which reuses its own takes_value predicate for it. Folds the now-redundant tokenize_dialect and tokenize_with_separate_value into the same options struct so the module doesn't keep growing one wrapper function per axis. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
reads_piped_stdin treated any non-terminal stdin as "the engine will read stdin," including a non-pipe redirect like `< /dev/null`. Real grep/rg still search the cwd/default path in that case, not stdin. This routed rtk rg -z foo < /dev/null (and, pre-existing, any rtk rg/ grep invocation with redirected-but-not-piped stdin and no explicit path) into the streaming path, which can't discover "multiple files matched" the way the buffered/grouped path can, so it silently dropped filenames from every line. stdin_is_pipe() checks the actual file type (FIFO) on Unix instead of just is_terminal(), falling back to the prior heuristic elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rule Reviewer asked that this system be documented in the relevant READMEs. Adds an Argument Tokenizer section to src/core/README.md covering what the module provides and the design rule two confirmed bugs this round came from: never reuse a sibling command's grammar predicate wholesale (stash_show_wants_patch's -u/-p conflation, requests_diff_show_raw_shape's list-drift). Cross-links from src/cmds/git/README.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per review feedback: many doc comments in this branch had grown longer
than the code they documented, narrating investigation history
("confirmed via Docker/real git X.Y.Z", commit hashes, issue numbers)
instead of stating the non-obvious fact a reader actually needs, or
simply restating what a well-named function/type already says.
Trimmed every multi-paragraph comment across the 5 files this branch
touches down to 1-2 lines, keeping only the load-bearing why. Also:
- dropped comments that only restated a well-named call
(restore_double_dash, dotnet_takes_value's Dialect::Msbuild) or a
callee's own doc (run_diff/run_show's tokenize call sites duplicated
requests_diff_show_raw_shape's rationale)
- renamed git_log_tokens/dotnet_tokens to tokenize_git_log_args/
tokenize_dotnet_args so the call site reads as an action without
needing a comment
- trimmed several "Regression: X used to..." test comments down to
the bare invariant under test
- documented the tokenizer's design in src/core/README.md, kept light
(no consumer list, no bug writeup, just the rule and one example)
Comment-line counts in the diff against develop: from roughly 18% of
added lines down to ~16%. No logic changes anywhere in this commit:
comments, blank lines, and two function renames only, verified via
cargo fmt/clippy/test after every step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KuSh
force-pushed
the
feat/arg-tokenizer
branch
from
August 26, 2026 21:34
46c3b51 to
be0c3d7
Compare
Collaborator
Author
|
@aeppling took all of this into account |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces
src/core/arg_tokenizer.rs, a shared CLI-argument tokenizer, and migratesgit.rs,search.rs(grep/rg),dotnet_cmd.rs, andgolangci_cmd.rsoff ad hoc string-matching flag/value/---boundary detection — the repeated source of bugs across this codebase (each of those files had accumulated its own one-off--boundary and flag-vs-value bugs independently).The tokenizer classifies args into
Long/Short/Positional/DashDashtokens, tracks attached vs. linked (separate-token) values, and models two real CLI grammars viaDialect(POSIX-ish for git/rg/golangci-lint, MSBuild-ish for dotnet). Per-tool grammar quirks beyond the basetakes_valuepredicate are opt-in via aTokenizeOptionsstruct passed totokenize_with_options, defaulting to the common case so each caller only sets what it needs:takes_separate_valuefor aShortflag that doesn't always accept a separate-token value (git), andclaims_literal_dash_dashfor a value-taking flag that claims a literal--as its value instead of treating it as the end-of-options boundary (grep/rg — confirmed per-tool, not per-flag: git/cargo both reject--as a pending value regardless of which flag is asking).Real bugs found and fixed (each with a regression test verified fail-before-fix / pass-after-fix)
git.rs
--boundary awareness added torun_diff,run_show,run_log,run_branch,run_stash show— a pathspec/filename literally named like a flag (--stat,-p, etc.) after--was previously misread as the real flag.run_diff/run_shownow share most ofrun_log's raw-diff-shape flag list (--name-only,--raw,--stat, etc.) instead of a narrower 3-flag subset — except-p/-u/--patch, which stay on the compact path fordiff/show: unlikelog(whose default output has no diff content at all),diff/show's default output already is patch text, so passing-pexplicitly is redundant with the default rather than a request for an incompatible shape. (An earlier version of this same migration wrongly folded-p/-u/--patchinto the shared list too, which meantgit diff -p/git show -p <rev>silently bypassed RTK's compaction — caught by/code-review highand fixed before merge.)git stash show -uno longer misread as patch mode:-umeans--include-untrackedforstash show, unlikegit logwhere-uis a-psynonym — the stash-show detector had copied log's mapping, so-u's real stat-only output was silently rendered as empty instead. Confirmed against real git; caught by reviewer testing, fixed before merge.checkout -b/-Bglued short-flag form (-bmy-branch) confirmed against real git and pinned.-u's upstream value now links correctly instead of leaking as an unlinked positional.has_limit_flagno longer misfires on a clustered-nwith no captured value.-M/-U/-C/-Bare attached-value-only (never separate-token, confirmed evengit log -U 3fails against real git),-n/-laccept a separate-token value only when standalone (-n 2works,-cn 2fails with "ambiguous argument" against real git) — this needed a dedicatedtakes_separate_valuepredicate (git.rs'slog_takes_separate_value, passed viaTokenizeOptions) since it's a git-specific parser quirk, not a POSIX/GNU universal (confirmedgrep -im 2does accept it clustered).dotnet_cmd.rs
/-prefixed multi-segment paths (/abs/path/Project.csproj) no longer misclassified as a flag (confirmed via a real dotnet 9 SDK).detect_test_runner_modeno longer scans flag values or post---filter expressions as candidate project paths.dotnet_takes_valueexpanded with--filter,-c/--configuration,-f/--framework,-r/--runtime,-a/--arch,--os(confirmed via a real dotnet 9 SDK).has_nologo_argno longer treats a broken attached-value spelling (-nologo:true) as "already present".dotnet_has_flag/dotnet_has_loose_flagnaming swapped so the safe, strict lookup gets the short name (the loose one previously invited exactly the kind of misuse a prior commit in this branch had to fix).search.rs (grep/rg)
-L,-z,-T, and-rall made engine-aware — each means something different for grep vs. rg (confirmed via realgrep --help/rg --helpand direct runs); e.g. rg's-ris--replace <value>while grep's is--recursive(boolean).has_format_flagcomputed inextract_pattern_path's own single token pass instead of re-tokenizing a second time.has_short_flag's reconstructed-string ambiguity fixed:extract_pattern_pathnow computesshow_file/show_line/contextdetection (a newDetectedFlags) directly from tokens in that same single pass, instead of re-scanning a reconstructed args string that could misread a value-taking flag's own value as an unrelated short flag.grep -e --/rg -e --now match real grep/rg: any value-taking flag in these tools claims a literal--as its value (confirmed for both engines, short and long forms —-A/-m/-e/--context/--file), instead of treating it as the end-of-options boundary.rtk grep -e -- fpreviously searched/dev/nullfor pattern"f"; now matches real grep's"a -- b". Caught by reviewer testing, fixed before merge.reads_piped_stdinnow checks for an actual pipe/FIFO instead of merely!is_terminal(). A non-pipe stdin redirect (e.g.< /dev/null) with no explicit path was misread as "the engine reads stdin", routing into the streaming path, which can't discover "multiple files matched" the way the buffered path can — silently dropping filenames from every line. Fixes the reportedrg -z foo < /dev/nullcase and the same pre-existing bug for plainrg/grep(not-z-specific —-zjust used to dodge this path via an accidental format-flag passthrough shortcut this PR itself removed). Caught by reviewer testing, fixed before merge.golangci_cmd.rs
restore_double_dashgap fixed (missing entirely).has_output_flagcorrectly scoped to run-level flags via a dedicatedgolangci_run_takes_value(verified against real golangci-lint 2.13.1 via Docker), including the--out-formatlegacy flag it was initially missing.Documentation
src/core/README.mdnow documentsarg_tokenizer.rsand the design rule this branch's grammar-sharing bugs kept surfacing: one grammar per subcommand, never reuse a sibling's predicate wholesale (e.g.-umeans-pingit logbut--include-untrackedingit stash show).Known, documented limitations (deliberately not fixed — each has a concrete reason in its own doc comment)
/app,/tmp) are structurally indistinguishable from a switch name — confirmed real MSBuild itself needs a filesystemstat()call to resolve this, which this pure-function tokenizer deliberately doesn't do.dotnet_cmd.rs'sdetect_test_runner_mode(explicit_projects) has no filesystem backstop — it relies entirely ondotnet_takes_value's allowlist staying exhaustive for every dotnet flag that takes a separate-token value; a real value-taking flag missing from it whose value happens to end in.csproj/.fsproj/.vbprojwould be misread as an explicit project path.log_takes_value/log_takes_separate_value,dotnet_takes_value's subcommand-unawareness, golangci's double-tokenize betweenfind_subcommand_index/has_output_flag) were reviewed and left as-is — each either has no confirmed failure scenario, or a proper fix requires a larger restructuring disproportionate to the actual risk.Explicitly out of scope
src/cmds/go/go_cmd.rs,src/cmds/rust/cargo_cmd.rs,src/discover/registry.rs, andsrc/cmds/system/ls.rsshare the same bug class this PR fixes elsewhere but are untouched by this diff — pre-existing, not a regression from this branch, best handled as separate follow-up work. Along with the documented limitations above, these are tracked as planned follow-up work once this PR merges.Test plan
cargo fmt --all --check,cargo clippy --all-targets(zero warnings),cargo test --all(2793 tests) all pass/code-review high(multi-agent adversarial review) run against this branch, iterating fix → re-review until convergence, including targeted re-reviews after later fixes were pushed; remaining findings are the documented limitations above or lower-priority cleanup with no concrete failure scenariogit stash show -u,grep -e --,rg -zfilename-dropping) plus doc/comment-density feedback — all fixed, see above