ripgrep (rg) Cheat Sheet and Guide
The ritual goes like this. grep -rn "fetchUser" ., Enter, and then a wall of minified
JavaScript from node_modules scrolls past for eleven seconds, followed by a line from a
build artefact you deleted in spirit last March, followed by Binary file .git/index matches.
The one line you wanted is in there somewhere, and it is not on screen any more.
ripgrep — the binary is rg — is what happens when somebody decides the defaults were the
problem, not you. It searches recursively without being asked, skips everything your
.gitignore skips, ignores hidden files and binaries, uses every core you have, and prints
results grouped by file with line numbers and colour. Written in Rust by Andrew Gallant, it is
the engine behind VS Code’s search panel, which means you have almost certainly used it without
knowing.
The short answer to “how do I use ripgrep” is rg pattern. The long answer is the eight or so
flags that turn it from a faster grep into something you can aim, plus the four reasons it
will one day refuse to find a file you can see with your own eyes.
ripgrep cheat sheet
Section titled “ripgrep cheat sheet”The shape is rg [options] PATTERN [PATH...]. No path means the current directory, searched
recursively. Quote patterns and globs, so the shell doesn’t get to them first.
Searching
Section titled “Searching”| Command | Does |
|---|---|
rg TODO |
Search this directory and everything in it |
rg TODO src/ lib/ |
Search only these paths |
rg -i todo |
Ignore case |
rg -S todo |
Ignore case unless the pattern has a capital |
rg -F 'a.b()' |
Literal string — . and ( mean themselves |
rg -w id |
Whole words: id, not width |
rg -v TODO |
Lines that don’t match |
rg -e foo -e '-bar' |
Several patterns, or one that starts with - |
Choosing which files
Section titled “Choosing which files”By default rg skips gitignored, hidden and binary files. Why rg can’t see your file explains each rule; these flags turn them off or add new ones.
| Command | Does |
|---|---|
rg -t py import |
Python files only |
rg -T js TODO |
Everything except JavaScript |
rg --type-list |
Every built-in type and the globs behind it |
rg TODO -g '*.tsx' |
Only paths matching a glob |
rg TODO -g '!node_modules/' |
Skip a directory, however deep |
rg -u TODO |
Include gitignored files |
rg -uu TODO |
…and hidden files, .git included |
rg -uuu TODO |
…and binary files: roughly grep -r |
rg --hidden -g '!.git' TODO |
Hidden files, minus the .git directory |
rg -L TODO |
Follow symlinks |
rg -z ERROR app.log.gz |
Search inside compressed files |
Output
Section titled “Output”| Command | Does |
|---|---|
rg -l TODO |
Names of files with a match |
rg --files-without-match TODO |
Names of files without one |
rg -c TODO |
Matching lines per file |
rg --count-matches TODO |
Matches per file |
rg -o 'v\d+' |
Only the matched text, one per line |
rg -C 3 panic |
3 lines of context — -A after, -B before |
rg -M 200 TODO |
Truncate lines over 200 columns |
rg --files |
List the files rg would search, without searching |
rg -l0 TODO | xargs -0 wc -l |
Hand filenames on safely, spaces and all |
rg --json TODO |
One JSON object per event, for programs |
Regex and replace
Section titled “Regex and replace”-r changes only what rg prints. The last row is how you edit the files themselves — see
search and replace.
| Command | Does |
|---|---|
rg -P 'foo(?=Bar)' |
PCRE2: look-around and backreferences |
rg -U 'fn main\(\) \{\n' |
Match across lines |
rg 'v(\d+)' -r 'version $1' |
Show each match with a replacement applied |
rg -o 'id=(\d+)' -r '$1' |
Print just a capture group |
rg -l0 old | xargs -0 perl -pi -e 's/old/new/g' |
Replace in the files themselves |
Scripts and debugging
Section titled “Scripts and debugging”| Command | Does |
|---|---|
rg -q TODO |
No output — exit 0 on a match, 1 without, 2 on error |
rg --sort path TODO |
Stable order, at the cost of a single thread |
rg --debug TODO |
Say why each file was searched or skipped |
rg --no-config TODO |
Ignore your config file for this run |
If a search comes back empty and you’re sure it shouldn’t, try -uu before anything else.
It answers most of the “rg is broken” questions on the internet, and --debug answers the rest.
Installing it
Section titled “Installing it”brew install ripgrep # macOSsudo apt install ripgrep # Debian, Ubuntusudo dnf install ripgrep # Fedora, RHELwinget install BurntSushi.ripgrep.MSVC # WindowsThe package is ripgrep everywhere; the command is rg everywhere, including Debian, which for
once has not renamed anything. Check what you got:
rg --versionThe line to look for is features:+pcre2. Homebrew’s build has it; some distribution builds
have historically not, and without it the -P flag below does nothing but apologise.
What it looks like
Section titled “What it looks like”Run in a terminal, rg TODO groups matches under each filename, numbers the lines, and
highlights the match:
src/api/users.js2: // TODO: retry on 503
src/main.js3:// TODO: remove before the demoTwo files. Not the TODO in node_modules/left-pad, not the one in the minified bundle under
dist/ — both listed in .gitignore, so ripgrep never opened them.
Pipe the output anywhere and the format changes to grep’s familiar path:line shape with no
colour and no line numbers, which is what xargs, cut and your editor’s quickfix list expect.
If you want line numbers in a pipe, ask for them with -n; if you want the grouped layout in a
pipe, --heading. Colour into a pager is rg --color=always TODO | less -R, though rg -p —
--pretty — does colour, headings and line numbers in one flag.
Why rg can’t see your file
Section titled “Why rg can’t see your file”This is the whole of ripgrep’s personality, and the source of every “rg is broken” thread on the internet. By default it skips four things:
- Anything matched by
.gitignore,.ignoreor.rgignorefiles - Hidden files and directories — anything starting with a dot
- Binary files
- Symlinks
Each -u peels off a layer:
| Flag | Also searches | Long form |
|---|---|---|
| (none) | — | |
-u |
Ignored files | --no-ignore |
-uu |
…and hidden files | --no-ignore --hidden |
-uuu |
…and binary files | --no-ignore --hidden --binary |
-uuu is roughly grep -r, which is a useful way to think about what ripgrep is doing for you
the rest of the time. Symlinks are separate: -L (--follow) follows them.
Four details that are not obvious and will each cost you ten minutes exactly once:
--hidden means .git too. Ask for hidden files and ripgrep will cheerfully search your
repository’s object database, hooks and all. The clean version excludes it by glob:
rg --hidden -g '!.git' API_KEYThat combination is worth putting in a config file so it becomes the default.
.gitignore only counts inside a git repository. Copy a project somewhere without its
.git directory — an unpacked tarball, a Docker build context — and ripgrep stops honouring the
.gitignore, so dist/ comes back. --no-require-git makes it respect the file anyway.
node_modules is not special. ripgrep skips it only because your .gitignore lists it. A
project that doesn’t ignore it gets it searched in full. When you want something skipped for
searching but still tracked by git — fixtures, vendored code, a generated schema — list it in an
.ignore file, which uses the same syntax and which git itself never reads.
A glob can’t reach into an ignored directory. ripgrep never descends into dist/, so
rg TODO -g 'dist/**' finds nothing and tells you “No files were searched”. Name the path
instead: rg TODO dist/. An explicitly named path is always searched, ignore rules or not.
When you genuinely can’t work out why a file is being skipped, --debug prints the reason for
every decision it makes, which is verbose, and also the answer.
Filtering by file type, glob or directory
Section titled “Filtering by file type, glob or directory”By file type. -t restricts to a type, -T excludes one. ripgrep ships definitions for a
couple of hundred languages, so you don’t have to remember that TypeScript is also .mts,
.cts and .tsx:
rg -t py 'import requests'rg -t ts -t js useEffect # several typesrg -T js TODO # everything except JavaScriptrg --type-list | rg '^ts:' # what counts as "ts"Missing one? --type-add defines it on the spot, and a config file makes it permanent:
rg --type-add 'web:*.{html,css,js}' -t web 'font-family'By glob. -g includes paths matching a glob, and a leading ! excludes them. Globs use
.gitignore syntax and can be repeated:
rg TODO -g '*.tsx'rg TODO -g '!*_test.go' -g '!testdata'rg TODO -g 'src/**'Always quote globs, or your shell will expand them first and ripgrep will receive a list of
filenames it didn’t ask for. Globs are case-sensitive; --iglob is the case-insensitive
twin, for the repository where someone saved README.MD.
By directory. Excluding a directory is a glob with a trailing slash, which matches
directories only — so node_modules.txt survives — at any depth. A leading slash pins it to the
top of the search instead:
rg TODO -g '!node_modules/' # every node_modules, however deeprg TODO -g '!/vendor/' # only ./vendorIf you’re excluding the same directory every time, it belongs in an .ignore
file rather than your fingers.
Matching
Section titled “Matching”Most of ripgrep’s matching flags are grep’s — -i, -F, -w, -v, -e — and they’re in the
cheat sheet, along with smart case, which grep doesn’t have. The one not listed
there is -x, for whole-line matches. Two deserve more than a table row.
Smart case is the first. rg -S todo finds TODO, Todo and todo; rg -S Todo finds only
Todo, on the reasonable assumption that if you typed a capital you meant it. It is so plainly
the right default that it goes straight into the config file below.
-F matters more than it does with grep, because people search code for code, and code is full
of regex metacharacters. rg 'a.b()' is a regex matching axb followed by nothing in
particular; rg -F 'a.b()' is the method call you pasted.
To search for something that looks like a flag, end the options first:
rg -- --forcerg -e '-v'The regex dialect
Section titled “The regex dialect”ripgrep uses Rust’s regex crate by default, which is fast because it guarantees linear-time
matching — and it guarantees that by refusing the two features that can’t offer it. Ask for
either and you get a clear error rather than a wrong answer:
rg 'foo(?=Bar)' # error: look-around ... is not supportedrg '(\w)\1' # error: backreferences are not supported-P switches to PCRE2 for that search, which supports both. --engine auto tries the fast
engine first and falls back to PCRE2 only when the pattern needs it — handy in a config file,
if you would rather not think about it.
Otherwise the syntax is what you’d expect from any modern regex flavour: \d, \w, \b,
[[:alpha:]], named groups as (?P<name>...), inline flags like (?i). Two things catch grep
users. There is no basic-versus-extended split — +, ?, | and () are always operators,
so \( is a literal bracket, as in grep -E. And \s never matches a newline, because by
default ripgrep searches one line at a time.
Matching across lines needs -U (--multiline):
rg -U 'fn main\(\) \{\n\s+//'Even then . stops at newlines. Add (?s) to the pattern, or --multiline-dotall, when you
want .* to run on through them — and be aware that (?s)fn.*api will now happily match from
the first fn in the file to the last api, because .* is greedy and the file is now one
long line as far as it is concerned. .*? is usually what you meant.
Getting the output you want
Section titled “Getting the output you want”The output flags are in the cheat sheet. A few earn a closer look, starting with one
that isn’t there: editor integrations want --vimgrep, which prints path:line:column:text
once per match, ready for a quickfix list.
--files deserves more fame than it has. It’s a gitignore-aware file lister, which makes it a
tidy input for anything else that wants a list of your project’s real files — including
fzf, which has spent years being fed worse lists than this.
-M is the minified-file fix. One matching line in a 400KB bundle is 400KB of output; -M 200
replaces it with [Omitted long matching line], and adding --max-columns-preview shows the
first 200 columns rather than nothing.
--json emits a begin, match and end object for every file, with line numbers, byte
offsets and the submatches themselves. It is verbose on purpose, and jq
takes it apart in one line — every match as path:line:
rg --json fetchUser | jq -r 'select(.type == "match") | "\(.data.path.text):\(.data.line_number)"'For handing filenames to another command, use NUL separators so a path with a space in it doesn’t turn into two arguments:
rg -l0 TODO | xargs -0 wc -lSearch and replace
Section titled “Search and replace”ripgrep has a -r (--replace) flag, and the first thing to understand about it is that it
never touches your files. It rewrites the output and nothing else. That makes it a preview
tool and an extraction tool, and both are genuinely useful:
rg 'fn (\w+)' -r 'func $1' # preview a renamerg -o 'user_id=(\d+)' -r '$1' app.log # pull out just the IDs-o with -r is the combination to remember: print only the capture group, one per line, ready
for sort | uniq -c. It replaces a good share of the sed and awk one-liners you’ve been
copying from Stack Overflow for years.
Three details about the replacement string:
- Capture groups are
$1,$2, or$namefor a named group(?P<name>...). $1_v2is not$1followed by_v2. ripgrep reads the longest name it can, looks for a group called1_v2, doesn’t find one, and substitutes nothing. Write${1}_v2.- A literal dollar is
$$. Single quotes around the replacement keep your shell’s hands off it, which you want, because the shell has its own ideas about$1.
--passthru prints every line, matching or not, which with -r gives you the whole file with the
replacement applied — redirect that and you’ve edited a file, provided you write to somewhere
other than the file you’re reading.
For a real in-place edit across a project, let ripgrep find the files and something else change them:
rg -l0 'api_key' | xargs -0 perl -pi -e 's/api_key/apiKey/g'perl -pi -e rather than sed -i because it behaves identically on macOS and Linux, where
sed -i does not. It uses Perl’s regex dialect, not ripgrep’s, which for anything beyond a
literal word is worth a moment’s thought. Or install sd, which does the replacement with a
syntax no one has to look up. Either way, run it on a clean working tree, so git diff is your
review and git checkout . is your undo.
The ripgrep config file
Section titled “The ripgrep config file”ripgrep reads a config file only if you tell it where one is — there is no default location,
which is either admirable restraint or a missed opportunity, depending on how many machines you
have. Point RIPGREP_CONFIG_PATH at it:
export RIPGREP_CONFIG_PATH="$HOME/.config/ripgrep/rc"The file holds one argument per line, exactly as you’d type it but without shell quoting, and
lines starting with # are comments:
# Smart case: sensitive only when the pattern has a capital--smart-case
# Search dotfiles, but never the git object database--hidden--glob=!.git
# Stop minified files flooding the screen--max-columns=200--max-columns-preview
# Types ripgrep doesn't know about--type-add=web:*.{html,css,js}Flags on the command line are applied after the config file, so they win where the two clash.
When the config is the problem — and one day --hidden will be why you’re seeing results you
didn’t expect — --no-config ignores it for that run.
One more option worth knowing: --hyperlink-format turns filenames into clickable links in
terminals that support OSC 8, opening the match in your editor at the right line. It takes a
template or an alias — vscode, cursor, kitty, macvim, textmate and a few others —
so --hyperlink-format=vscode in the config is all it needs, provided your terminal
emulator is one of the ones that understands the escape.
The file itself belongs in your dotfiles repo, along with the line that points at it.
ripgrep in scripts
Section titled “ripgrep in scripts”The exit codes follow grep’s: 0 for a match, 1 for none, 2 for an error. -q suppresses
output and stops at the first match, so the idiomatic test reads the same as it does with grep:
if rg -q 'DEBUG = True' settings.py; then echo 'refusing to deploy with DEBUG on' >&2 exit 1fiInside an if condition a “no match” of 1 doesn’t trip set -e, which is exactly as intended
and covered, with the places it isn’t, in Bash strict mode. A bare
rg returning 1 on its own line will still end a strict script, so || true when nothing
found is a normal outcome.
Two more habits for scripts. Output order isn’t stable, because files are searched in parallel;
--sort path fixes the order at the cost of running on a single thread. And ripgrep reads
standard input when it’s given no path and something is piped in, so it slots into the middle
of a pipeline like any other filter:
kubectl logs deploy/api | rg -i 'timeout|refused'For a stream that never ends — tail -f on a log — mind the buffering. Printing to a terminal,
ripgrep flushes every line; piping into another command, it buffers in blocks, so matches
arrive in bursts minutes apart. --line-buffered fixes it:
tail -f app.log | rg --line-buffered ERROR | rg -v healthcheckripgrep vs grep: six things that catch grep users
Section titled “ripgrep vs grep: six things that catch grep users”1. -r is not recursive. Recursion is the default, and -r means --replace. It takes an
argument, so rg -r TODO src doesn’t search src for TODO — it searches the current
directory for src and replaces it with TODO in the output. Usually that means no output and
exit code 1, which looks exactly like “nothing found”. This is the single most expensive habit
to bring over from grep.
2. -uu searches .git. It includes hidden files, and your repository’s internals are
hidden files. If rg -uu suddenly returns matches from hook samples and packed refs, that is
why — -g '!.git' is the fix.
3. -c counts lines, not matches. A line with three matches counts once. --count-matches
counts all three.
4. It isn’t POSIX grep, so don’t alias it. The flags overlap enough to feel familiar and
differ enough to hurt: -r, above, is only the most dramatic example. The starter
pack makes the same argument
for fd and find. Type rg. It’s shorter anyway.
5. The binary skip is silent. A match inside a file ripgrep decides is binary — anything
with a NUL byte in it — isn’t reported at all in a directory search. -uuu, or -a on a
named file, when it matters. -z handles the compressed case: rg -z ERROR app.log.gz searches
inside gzip, xz, bzip2, zstd and friends by shelling out to the matching decompressor.
6. Ignore rules are inherited from directories above. A .gitignore in a parent directory
applies to everything below it, and so does your global git excludes file. When something is
missing and you can’t see why, --debug will name the file responsible.
When ripgrep isn’t the answer
Section titled “When ripgrep isn’t the answer”- On somebody else’s server, use grep. It’s there, it’s POSIX, and it’s what the runbook assumes. ripgrep is a reason to enjoy your own machines, not a reason to forget the tool that’s on all of the others.
- For finding files by name, use
fd— orrg --files -g '*name*'if you only need it once. Searching contents to find a filename is the long way round. - For structural code search — “every call to
fetchwhose second argument has nosignal” — a regex is the wrong instrument.ast-grepmatches on syntax trees and knows what a function call is, which no amount of\s*will teach ripgrep. - For a whole-codebase index, your editor’s language server and
git grepboth have opinions.git grepin particular searches tracked files only, and can search any commit, not just the working tree —git grep TODO v1.2.0— which ripgrep can’t.
Where next
Section titled “Where next”ripgrep is at its best feeding something interactive: fzf’s live-search
recipe turns it into a fuzzy code browser
with a preview pane in a dozen lines. The modern CLI starter
pack has the rest of the family — fd for filenames, bat
for the preview, delta for the diff after the replacement. And if the aliases and the
RIPGREP_CONFIG_PATH line need somewhere to live, Zsh without the
bloat is the forty-line .zshrc to put them in.