
The easy review is “ripgrep is fast.” The useful review asks whether two commands search the same files, interpret the same expression, and return an exit status a script can trust.
This hands-on pass used ripgrep 15.2.0 and GNU grep 3.12 on 13 August
2026. GNU grep 3.12 is the current upstream release 4, not the older
3.11 shipped by the Ubuntu host. It was built from the official tarball
(sha256:2649b27c…08a07b9) without Perl-regexp support. ripgrep reported PCRE2
10.45 with JIT and runtime SSE2, SSSE3, and AVX2.
The x86-64 lab host ran Ubuntu 24.04, Linux 6.8.0-100-generic, an AMD EPYC 9534, and Zsh for orchestration. The review was one functional pass, not an endurance run. Its fixture was an 11 MB Git repository with 400 generated log shards, ten matching normal files, one matching ignored file, and one matching hidden file. No sponsorship, vendor access, or synthetic speed claim is involved.
The default result is a scope result
rg -l NEEDLE corpus
grep -R -l NEEDLE corpusObserved file counts:
| Command | Matches | Default scope in this fixture |
|---|---|---|
rg -l NEEDLE corpus |
10 | normal searchable files; Git ignore and hidden rules applied |
grep -R -l NEEDLE corpus |
12 | normal, ignored, and hidden fixture files |
Neither answer is inherently more correct. ripgrep recursively searches while
respecting common ignore rules and skipping hidden and binary files by default
1. GNU grep’s -R recursively follows its own documented directory
and device behavior and does not treat .gitignore as search policy
3.
For code search, ripgrep’s default often matches intent. For incident response, forensics, or “find this byte sequence anywhere under this tree,” silently forgetting ignored output can be a serious miss.
Equalize scope before comparing output or time
rg -uuu -l NEEDLE corpus
grep -R -l NEEDLE corpusBoth returned the same 12 fixture files. ripgrep’s -u flags progressively
reduce filtering; -uuu searches ignored, hidden, and binary content. That is a
powerful override, not a good default for every tree: it can enter .git, build
artifacts, credentials, and huge binary directories.
Our /usr/bin/time runs both rounded to 0.00 s. That clock resolution is not
evidence that they have identical performance. A performance conclusion would
need a much larger representative corpus, repeated warm and cold runs, fixed
filesystem/cache conditions, and matching regex and file scope.
Pattern language is another boundary
Both tools support regular expressions, but not one identical engine. ripgrep’s default regex engine deliberately omits some expensive features such as look-around and backreferences; it can opt into PCRE2 when built with support 1.
rg --pcre2 '(?<=user=)[a-z]+' app.log
printf 'user=alice\n' | grep -Po '(?<=user=)[a-z]+'Those commands are not portable equivalents. The tested ripgrep build accepted
the first; the tested GNU grep build rejected -P with status 2 because it was
configured without Perl-regexp support. GNU grep’s -P and ripgrep’s --pcre2
are implementation/build options outside the portable POSIX grep contract
5. For reusable scripts, state the required tool and test the exact
pattern on the deployed build.
For a fixed string, say so:
rg -F 'status=[500]' logs/
grep -R -F 'status=[500]' logs/-F removes regex interpretation. It is clearer when punctuation is literal.
Exit status matters more than colored output
Both commands conventionally use:
0when at least one selected line matched;1when the search completed with no selected lines;- a value greater than
1for an error.
Preserve that distinction in scripts:
if rg -q -F 'READY' service.log; then
printf '%s\n' 'ready marker found'
else
status=$?
if [ "$status" -eq 1 ]; then
printf '%s\n' 'marker not present'
else
printf 'search failed: status=%s\n' "$status" >&2
exit "$status"
fi
firg -q can stop after a match. GNU grep has -q too. Do not collapse “no
match” and “file unreadable” into the same operational result.
Output and ergonomics
ripgrep provides code-search-oriented defaults: recursive traversal, line numbers and color when useful, concise paths, type filters, and ignore support.
rg -n -C 2 --type js 'deprecatedApi' src/
rg --files -g '*.service'GNU grep is a smaller portability assumption on Unix-like systems and remains a strong stream and file filter:
journalctl -b --no-pager | grep -F 'Out of memory'
LC_ALL=C grep -R -n --include='*.conf' 'Listen' /etc 2>/dev/nullLocale can affect character classes, ranges, and performance. Pin LC_ALL=C
only when byte-oriented matching is truly the contract.
Verdict
Use ripgrep as the interactive default for source trees when project ignore rules describe intended search scope. Use GNU grep when portability, streaming, or an existing POSIX-oriented script matters. In security and recovery work, name the scope explicitly regardless of tool.
The measured lesson is not a speed trophy: default recursive commands searched different files; equalized commands agreed. A trustworthy benchmark or script begins there.