All articles

ripgrep 15.2 vs GNU grep 3.12: A Search Behavior Baseline

10 minutes read


Linux publication

Share this article

𝕏✉

Illustration for ripgrep 15.2 vs GNU grep 3.12: A Search Behavior Baseline

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 corpus

Observed 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 corpus

Both 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:

  • 0 when at least one selected line matched;
  • 1 when the search completed with no selected lines;
  • a value greater than 1 for 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
fi

rg -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/null

Locale 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.

Sources and further reading
  1. ripgrep user guide
  2. ripgrep 15.2.0 release
  3. GNU Grep manual
  4. GNU grep 3.12 release announcement
  5. POSIX grep specification