Repository navigation
fix(search): strip ripgrep -r/-R so rg --replace no longer corrupts output - #3162
MichelKazi wants to merge 1 commit into
Conversation
…utput rtk forwarded -r/-R to the engine as grep's recursive flag, but ripgrep reads -r as --replace (value-taking). So `rtk rg -rn foo` ran rg's replace with "n", rewriting every match and producing interleaved-n garbage; `rg -rln PATTERN` replaced matches with "ln". ripgrep is recursive by default, so grep muscle memory silently corrupted output with no error, including in tee recovery files. Add strip_rg_replace, called only for Engine::Rg, to drop -r/-R/--replace and --recursive after arg extraction. grep keeps recursive pass-through unchanged. Signed-off-by: michelkazi <michel.m.kazi@gmail.com>
pszymkowiak
left a comment
There was a problem hiding this comment.
Thanks for tackling this — the diagnosis is right (rg's -r/-R is --replace, not recursive) and strip_rg_replace itself is well-built (handles --replace X, --replace=X, short clusters; correctly scoped to Engine::Rg only, so grep recursion is untouched). rtk rg -rn foo is fixed. ✅
However, tested on a compiled build, the fix is incomplete — any cluster combining -r with a format flag still corrupts output:
rtk rg -rl class file → "l Foo" (grep -rl muscle memory — very common)
rtk rg -rc class file → "c Foo"
rtk rg -ro class file → "o Foo"
rtk rg -rln tee file → "guaranln" ← the exact -rln example from this PR's description
Root cause: the three passthrough(&timer, engine, &args, …) calls (search.rs:446, 463, 474) re-run with the original, unstripped args. strip_rg_replace only touches extra_args (the compression path). So as soon as has_format_flag matches (-rl/-rc/-ro/-rq…), we passthrough with the raw -r… and rg re-applies --replace. has_format_flag's own comment says clusters like -rl should "route through" — but routing through passthrough re-introduces the very bug.
Suggested fix: for Engine::Rg, strip -r/-R/--replace from the args used by passthrough too (e.g. compute the stripped full-arg vector once and pass it to passthrough when the engine is rg), and add end-to-end coverage for -rl, -rc, -ro, -rln.
Grep is unaffected (verified: rtk grep -rn stays recursive). Once the format-flag/passthrough path is covered this is good to go.
|
Thanks for the detailed report and fix — the diagnosis is correct, but I'm closing this in favour of #3681, which addresses the same bug a layer lower. #3681 introduces a shared arg tokenizer and makes
#3681 also removes On the behaviour itself: #3681 forwards Thanks again for the report — it's a real issue and well documented. |
Fixes #3161
rtk rg -r/-R silently rewrites match output (ripgrep --replace)
Summary
rtk rg -rn PATTERN FILEcorrupts output. Every match is replaced by the string that follows-r. With-rn, that string isn, so output turns into interleaved-ngarbage. With-rln PATTERN, the string isln, so the tokenteeprints asln,guaranteeprints asguaranln, and so on.The cause is flag handling in
src/cmds/system/search.rs. rtk forwards-rand-Rto the engine verbatim, treating them as grep's recursive flag. For ripgrep,-r/-Ris--replace(value-taking), not recursive. ripgrep is already recursive by default, so agents that typerg -rnout of grep habit trigger a replace they never asked for.Version
rtk 0.43.0 (Homebrew).
developbranch has the same code path.Reproduction
Native ripgrep confirms rtk is passing
-rthrough as--replace:The interleaved-
nshape appears becausenis the replacement string and ripgrep applies it at every match position across the line.grep -rnis fine, because grep's-rreally is recursive. Onlyrgis affected.Root cause
src/cmds/system/search.rs:VALUE_FLAGS_SHORT(line 22) isb"ABCMTdfgjmt"and the doc comment on lines 20 and 21 says it lists "all rg short flags that take a value argument except-eand-r(stripped)". The intent is to strip-r.parse_cluster(line 84) never strips-r/-R. It accumulates them as boolean flags inraw_prefixand forwards them to the engine. The comment on lines 80 to 83 states r/R "pass through to grep (recursion is the agent's choice, not RTK's)".test_extract_cluster_keeps_randtest_parse_cluster_boolean_onlypin this behavior, so the tests currently encode the bug.For grep the pass-through is correct. For rg it is a data-corrupting default, because the same byte means two different things across the two engines rtk claims to run interchangeably.
Suggested fix
Make
-r/-Rengine-aware. ForEngine::Rg, strip-r/-Rand their value (they are recursion no-ops for rg and dangerous as replace). ForEngine::Grep, keep the current pass-through.Concretely,
parse_clusterneeds the engine, orextract_pattern_pathneeds a post-pass for rg that:-r/-R,r/Rletters from short clusters,--replaceand its value.Then update
test_extract_cluster_keeps_rand friends to assert the rg path strips-rwhile the grep path keeps it.Impact
Silent. rtk's own stdout looks structured (
N matches in M files:), so the corruption reads as real file content. Agents relying onrtk rg -rn(a common grep habit) get wrong file contents with no error. It also lands in the tee recovery files, so the "full output" recovery path is corrupt too.Workaround (no source change)
A PreToolUse shim wraps
rtk hook claudeand strips-r/-Rfrom rewrittenrtk rgcommands before they run. grep is left alone. This restores correct output without disabling rtk's filtering.