Replace grep and find with ripgrep and fd
Replace grep and find with ripgrep and fd for faster searches, automatic .gitignore support, and safer scripting patterns—with key gotchas explained.
Before you start
- ▸A terminal and a non-root user with sudo privileges
- ▸Basic familiarity with grep and find command syntax
- ▸Internet access or configured package repositories
ripgrep (rg) and fd are modern rewrites of grep and find that are faster, safer to script, and more ergonomic out of the box. ripgrep searches file contents using Rust's regex engine with SIMD acceleration; fd finds files and directories with sane defaults and a cleaner syntax. Neither is a complete drop-in replacement—they make different default choices—so this guide covers both the wins and the gotchas before you alias your muscle memory away.
Installation
Debian / Ubuntu
sudo apt install ripgrep fd-find
On Debian/Ubuntu, the fd binary is installed as fdfind to avoid a conflict with an unrelated fd package. Add a personal alias or symlink:
mkdir -p ~/.local/bin
ln -s "$(which fdfind)" ~/.local/bin/fd
# Make sure ~/.local/bin is in your PATH
Fedora / RHEL 9+ / Rocky
sudo dnf install ripgrep fd-find
Arch
sudo pacman -S ripgrep fd
Why They Are Faster
Speed comes from several concrete decisions, not just "Rust is fast":
- ripgrep skips binary files automatically, respects
.gitignoreby default, and uses SIMD-accelerated literal scanning. On large codebases it routinely outperforms GNU grep by 2–10×. - fd parallelises directory traversal across CPU cores, also respects
.gitignoreand hidden-file rules by default, and avoids the overhead of GNU find's expression parser for common cases. - Both tools skip
.git/directories automatically, which matters enormously in repository trees wherefind . -name "*.rs"would otherwise crawl object storage.
Replacing Common grep Patterns
Basic recursive search
GNU grep needs -r and you must remember to exclude irrelevant directories manually. ripgrep is recursive by default:
# Old way
grep -r "TODO" . --include="*.py" --exclude-dir=.git
# New way
rg "TODO" --type py
The --type flag understands dozens of language aliases (rg --type-list shows them all).
Case-insensitive and fixed-string search
# Case-insensitive
rg -i "error" /var/log/syslog
# Literal string, no regex interpretation (equivalent to grep -F)
rg -F "192.168.1.1" /etc/hosts
Show only filenames
rg -l "import requests" --type py
Multiline and context lines
# 3 lines of context above and below each match
rg -C 3 "panic" src/
# Multiline mode: match patterns that span newlines
rg -U "fn main\(\)[\s\S]*?\{" src/main.rs
Search hidden files and ignored paths
ripgrep skips dotfiles and .gitignore entries by default. Override with flags:
# Include hidden files
rg --hidden "secret" ~
# Ignore all .gitignore rules and search everything
rg --no-ignore --hidden "TODO" .
Replacing Common find Patterns
Find files by name
# Old way
find . -name "*.conf" -type f
# New way (glob by default)
fd -e conf
# Or with a regex pattern
fd '\.conf$'
Find by file type
# Directories only
fd -t d "node_modules"
# Executable files
fd -t x
Find and execute a command
fd's -x (execute once per file) and -X (execute once with all results as arguments) replace find ... -exec and find ... | xargs:
# Compress every .log file (one process per file)
fd -e log -x gzip {}
# Run a single command with all results (equivalent to xargs)
fd -e txt -X wc -l
{} is the placeholder for the matched path. fd also exposes {/} (filename), {.} (path without extension), and {//} (parent directory).
Find files modified recently
# Files changed in the last 24 hours
fd --changed-within 24h
# Files changed before a date
fd --changed-before 2024-01-01
Depth limiting
# Maximum depth of 2 directories
fd -d 2 -e yaml /etc
Scripting Patterns and Gotchas
Scripting with ripgrep
When using rg inside scripts, force plain output so terminal colour codes don't corrupt pipelines, and treat the exit code explicitly. rg exits 0 on match, 1 on no match, 2 on error—the same as GNU grep.
#!/usr/bin/env bash
set -euo pipefail
if rg --quiet --no-heading "CRITICAL" /var/log/app.log; then
echo "Critical entries found" >&2
rg --no-heading --color=never "CRITICAL" /var/log/app.log
fi
Use --color=never or --no-heading any time you pipe or redirect. The --quiet flag exits immediately on the first match, which is fast for existence checks.
Scripting with fd
fd prints NUL-terminated filenames with --print0, making it safe for filenames that contain spaces or newlines:
#!/usr/bin/env bash
fd --print0 -e jpg | while IFS= read -r -d '' file; do
echo "Processing: $file"
done
The .gitignore gotcha
Both tools respect .gitignore by default. This is usually what you want in a project, but it will silently omit files when you run them outside a git repo context or when searching directories where certain files are intentionally ignored (build artefacts, logs). Always add --no-ignore when you need exhaustive results in scripts:
fd --no-ignore --hidden -e log /var/log
Regex dialect differences
ripgrep uses Rust's regex crate, which does not support backreferences or lookaheads with arbitrary complexity. If you rely on PCRE features like (?<=...) lookbehind or backreferences, pass -P to enable the PCRE2 engine (requires ripgrep to be compiled with PCRE2 support, which most distro packages include):
rg -P "(?<=def )\w+" --type py
Verification
Confirm both tools are installed and working:
rg --version
fd --version # or fdfind --version on Debian/Ubuntu
Expected output is a version string like ripgrep 14.x.x and fd 9.x.x (exact numbers vary by distro and release).
Run a quick functional test in any directory with a git repo:
rg "TODO" --stats .
fd -e md --max-results 5
The --stats flag on ripgrep prints how many files were searched, skipped, and matched—useful for confirming ignore rules are behaving as expected.
Troubleshooting
- fd not found on Debian/Ubuntu: The binary is
fdfind. Create the symlink shown in the installation section or addalias fd=fdfindto your shell rc file. - Expected files missing from results: The most common cause is an active
.gitignoreor a parent.ignorefile. Run with--no-ignore --hiddento see everything, then narrow back down. - ripgrep skipping binary files: Use
--text(-a) to force ripgrep to treat all files as text. Be aware this can produce garbled output on true binary files. - Regex not matching as expected: Test your pattern in isolation with
rg -e 'pattern' <<< 'test string'. If you need PCRE2 features, confirm support withrg --pcre2-version. - Slow performance on network filesystems: Both tools parallelise aggressively. On NFS or FUSE mounts this can cause contention. Limit parallelism with
rg -j 1orfd -j 1.
Frequently asked questions
- Does ripgrep fully replace grep in all situations?
- No. ripgrep doesn't support searching a single piped stdin stream as naturally, lacks some POSIX grep options used in legacy scripts, and its default regex engine omits backreferences. Use grep for POSIX compliance in portable shell scripts and rg for interactive and project-level searches.
- Why does fd miss files that find returns?
- fd respects .gitignore and skips hidden files by default. Run fd --no-ignore --hidden to disable both behaviours and match find's exhaustive output.
- Can I use ripgrep to search compressed log files?
- Not natively. Decompress first with zcat or zstdcat and pipe into rg, for example: zcat app.log.gz | rg 'ERROR'.
- Is the -P (PCRE2) flag always available in rg?
- Only if ripgrep was compiled with PCRE2 support. Most distro packages include it; confirm with rg --pcre2-version. If the flag is absent, you'll get an error message saying PCRE2 is unavailable.
- How do I search files ignored by .gitignore without rewriting my .gitignore?
- Create a .ignore file in the project root (ripgrep and fd both read it). Entries in .ignore override .gitignore for search purposes without affecting git itself.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.