Comparison
rf does not replace ripgrep or fd. It wraps their engines and reports what a
naive one-shot invocation silently drops.
Against a naive search
On a synthetic corpus with the term planted in seven files behind different default filters:
| Invocation | Files found (of 7) | Exit on the same absent term |
|---|---|---|
rg pattern |
1 | 1 (looks like an error) |
rg -uu pattern |
4 | 1 |
rg -uu -a pattern |
5 | 1 |
rf content pattern |
7, each attributed | 0 with data: [] |
Against a shell pipe
For “find DB_DSN in *.config” with five real matches spread across a hidden
file, a gitignored file, and a binary file:
fd -e config | xargs rg DB_DSNfinds 1 of 5 (and prints a stray “binary file matches” line to stderr).rf find DB_DSN . --name configfinds 5 of 5, each attributed to its stage, and correctly leaves a true-negative*.configunflagged.
A pipe cannot attribute a miss to a stage: it erases fd’s exit code and conflates ripgrep’s “no files” with “no match.”
At scale
Run against a real working tree (a CPython checkout with compiled .pyc
artifacts gitignored, as a normal build leaves them), naive rg misses a large
fraction of real matches that rf recovers and attributes. Two measured terms:
| Term | naive rg |
rf content |
Recovered |
|---|---|---|---|
unittest |
1167 | 2220 | +1053 |
deprecated |
497 | 655 | +158 |
rf is also more precise than raw rg -uu here: rf never treats .git
internals as content, while rg -uu descends into .git/ and emits false
positives from commit messages and reflogs.
Numbers are from specific corpora and will vary with the tree. The point is not a fixed multiplier — it is that a naive search can miss most of the matches while still exiting 0, and
rftells you exactly what it missed and why.
When to reach for what
- Interactive grepping you trust your eyes on — plain
rg/fdare perfect. - A search whose result a program will act on —
rf, because “exit 0” from a naive search is not proof the tree was fully searched.