Repository navigation
feat(core): add arg_tokenizer and migrate git/grep/dotnet/golangci arg parsing onto it - #3681
Merged
Merged
Conversation
KuSh
force-pushed
the
feat/arg-tokenizer
branch
2 times, most recently
from
August 23, 2026 23:20
15efaf3 to
ed26781
Compare
2 tasks done
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 |
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 |
KuSh
added a commit
to KuSh/rtk
that referenced
this pull request
Aug 26, 2026
Per review feedback on rtk-ai#3681 (document new shared systems in their module README): lexer.rs's public functions are consumed well outside discover/ (hooks/permissions.rs, hooks/mod.rs, main.rs's rtk proxy), so document them as shared infrastructure rather than leaving them as an internal implementation detail.
This was referenced Aug 29, 2026
KuSh
force-pushed
the
feat/arg-tokenizer
branch
2 times, most recently
from
September 2, 2026 00:36
677e154 to
e8d823a
Compare
This was referenced Sep 2, 2026
Open
KuSh
force-pushed
the
feat/arg-tokenizer
branch
3 times, most recently
from
September 2, 2026 13:09
e081432 to
db95ec8
Compare
KuSh
marked this pull request as draft
September 2, 2026 17:06
KuSh
force-pushed
the
feat/arg-tokenizer
branch
from
September 2, 2026 23:06
580c3ea to
556b81a
Compare
KuSh
marked this pull request as ready for review
September 2, 2026 23:14
KuSh
marked this pull request as draft
September 2, 2026 23:29
KuSh
force-pushed
the
feat/arg-tokenizer
branch
from
September 3, 2026 04:36
556b81a to
d35cec3
Compare
KuSh
marked this pull request as ready for review
September 3, 2026 04:37
Merged
This was referenced Sep 12, 2026
Closed
This was referenced Sep 13, 2026
This was referenced Sep 13, 2026
This was referenced Sep 21, 2026
This was referenced Sep 26, 2026
This was referenced Oct 3, 2026
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.
What this adds
src/core/arg_tokenizer.rs— one place where RTK turns an argv slice into typed tokens, replacing the ad hocstarts_with('-')scans andfind("--")walks that each command module had grown its own copy of.A
Tokencarries its kind (Long,Short,Positional,DashDash), its flag name, its attached value (--flag=v), a link to its separate-token value (--flag v), and its index in the original argv, so a caller can always rebuild the exact arg a token came from.The tokenizer never guesses which flags take a value. The caller supplies the grammar as a
takes_value(kind, name) -> Option<ValueSpec>predicate scoped to that tool and that subcommand, and everything else follows from it: a flag's value is linked to the flag instead of counted as a positional,--is a real boundary, and a positional that looks like a flag stays a positional.One table answers every question the tokenizer has about a flag, rather than one predicate per question — that shape is how two lists start drifting apart, which is a bug class this PR fixes three times elsewhere.
ValueSpec::value()—--flag=vor--flag v, and a literal--is the boundary. The common case.ValueSpec::attached_only()—--flag=vonly; the next argument is never the value (git's-M/-U/-C/-Btake an optional attached number and nothing else).ValueSpec::solo_only()— aShortflag takes a separate value only when it is the whole argument:git log -n 2does,git log -pn 2does not..claiming_dash_dash()— lets a literal--be this flag's value. A per-tool split, not per-flag: grep and rg let any value-taking flag swallow it, git and cargo reject it whichever flag is asking.Two entry points, split by whether the caller has a grammar at all:
tokenize(args)for the callers asking only which arguments are flags and where--is, andtokenize_grammar(args, takes_value, dialect)for the rest. Two dialects —Posix(-abcclusters,--flag) andMsbuild(/flag:value, fordotnet).Helpers on top:
before_dashdash,dashdash_index,injection_point,is_digit_run,Token::value,Token::is_free_positional.What it changes
git.rs,search.rs(grep/rg),dotnet_cmd.rsandgolangci_cmd.rsare migrated onto it. Every fix below has a regression test that fails without it, and every claim about a tool's own behaviour was checked against that tool (git 2.53, GNU grep 3.12, ripgrep 15.2, .NET SDK 9 and golangci-lint 2.13.1 via Docker) rather than assumed.A
--boundary is a boundary.restore_double_dashnow runs once at git's dispatcher instead of per-subcommand, and every flag lookup that should stop at--does. A pathspec, worktree path or project file spelled like a flag is no longer parsed as one.A flag's value is not a positional.
dotnet test --filter Xno longer readsXas a project path;git branch -u origin/mainlinks its upstream;rg -r X TODO fno longer loses the pattern to--replace;grep -f patterns.txtreads its pattern file instead of searching for it.One grammar per tool, and per subcommand. grep and rg get separate value-taking tables transcribed from each engine's own
--help, because the overlap is smaller than it looks:-ris--recursivein grep and--replacein rg,-his--no-filenamein grep and--helpin rg,-Iis the reverse, and-N/-T/-z/-L/-Eall diverge. Within git,loganddifflikewise get their own:git log -cl 2is an error whilegit diff -wl 100is valid, and-umeans-pingit logbut--include-untrackedingit stash show.Output-shape detection reads tokens, not reconstructed strings.
git log -U<n>,--cc,-cand--remerge-diffrequest a patch and now pass through raw instead of being compacted into[+N lines omitted];-Wdoes not request one and no longer dumps the unfiltered history.--check,--exit-codeand--quietkeep their reports instead of being thrown away by RTK's own stat probe.git worktree repairreaches git instead of being answered with a worktree listing, andworktree list --porcelainpasses through instead of being replaced.Injection checks recognise every spelling of what they look for.
dotnet --report-trx:true,/l:trxand-l trxall count as "already present", so RTK stops double-injecting a logger;/flag:valueMSBuild switches can no longer swallow the next token as a value; golangci's output-flag check covers the full set of sink flags and is scoped to run-level ones.Design rule
The rule the migration kept running into is documented in
src/core/README.md: one grammar per tool and per subcommand — never reuse a sibling's predicate because it looks close enough..claude/rules/rust-patterns.mdgains a matching rule 6, so new commands, new flags and fixes to arg handling go through the tokenizer rather than adding another bespoke scan.What is not in this PR
src/cmds/system/ls.rs,src/cmds/rust/cargo_cmd.rs,src/cmds/go/go_cmd.rs,src/cmds/python/ruff_cmd.rs,src/discover/registry.rs. Same bug class, pre-existing, each its own follow-up.go_cmd.rsneeds anOsStringconversion first, since the tokenizer is deliberatelyAsRef<str>-only.ValueSpeccarries no semantic tag for what a value means — only how it attaches. Akindfield (Path, or aMaybePaththat asks for a filesystem check) drops in as one more field when a consumer appears, which is the headroom the struct was shaped for. Nothing reads such a tag today: every path-ish use in these four modules is keyed on a role — golangci's output sinks, search's pattern file, dotnet's results directory — and a genericPathtag would be strictly less precise than the name-keyed checks it would replace.Dialect::Msbuildfolds ASCII case for every flag name, but its only consumer mixes two grammars: MSBuild switches really are case-insensitive, dotnet's own options are not. A wrong-case spelling is rejected by dotnet either way, so the only cost is RTK suppressing an injection for a command that will fail regardless.Tokenexposes the prefix as two bools (double_dash,slash) rather than the three-state enum it holds internally, so consumers spell "single dash" as a double negative./app,/tmp) are structurally indistinguishable from a switch name. Real MSBuild resolves this with a filesystemstat(); this tokenizer is a pure function and does not.dotnet_takes_value's allowlist is subcommand-unaware and is whatexplicit_projectsrelies on: a value-taking flag missing from it whose value ends in.csprojwould be read as an explicit project path.--diff-merges' routing is static.logsends it to the raw passthrough on the flag alone, because a predicate cannot know whether the revision walk contains a merge; on a merge-free walk that costs compaction for a flag which provably changes git's output by zero bytes. The alternative — compacting a combined diff whose two marker columnscompact_diffmisreads — is silent corruption rather than verbosity, so this is the deliberate side to err on.git showrepeats its summary line under--oneline, and once per parent under--diff-merges' single-column formats, becauseshow_cmdputs RTK's flags ahead of the user's and git repeats the header per parent even under--no-patch. Both are pre-existing and byte-identical on the merge-base; both are cosmetic against a compacted body.Issues
git worktree repairis routed to list mode: prints the worktree list, exits 0, repairs nothing #3710 —git worktree repairis routed to list mode. Verified: the stale gitdir pointer is now actually repaired.git log -palready passed through before this branch, but-U<n>,--cc,-cand--remerge-diffwere still being compacted into[+N lines omitted]. They now pass through too.rtk grep -hprints help instead of searching — clap intercepts GNU grep's -h (--no-filename), exit 0, no native fallback #2391 /rtk grepclaims-h,-l,-mand-t, which belong to grep/rg #3663 — the display side of-h/--no-filename(and rg's-I) is implemented here and works for every spelling clap does not intercept. The bare-hstill never reaches the module; that needsdisable_help_flagon theGrep/Rgvariants, which is a separate UX decision.Test plan
cargo fmt --all --check,cargo clippy --all-targets(zero warnings),cargo test --all(3074 unit tests plus every integration suite)git/grep/rginvocations per round, including fuzzed flag combinations compared byte-for-byte against the real enginesLC_ALL=C— golangci-lint 2.13.1 on clean and issue-producing modules, .NET SDK 9 on passing and failing test projects, and .NET SDK 10 for MTP-native. Exit codes match the real tool in every case, including failures; no regressions found