cheat sheet
ripgrep (rg)
Modern grep replacement written in Rust. Respects .gitignore, searches hidden files optionally, supports PCRE2, and is significantly faster on large codebases.
ripgrep (rg) — Fast Recursive Search
What it is
ripgrep (invoked as rg) is a free, open-source Rust-based line-oriented search tool maintained by Andrew Gallant on GitHub. It recursively searches directories for a regex pattern, respects .gitignore by default, and is consistently faster than grep, ack, and ag on large codebases thanks to its use of the Rust regex engine and parallel directory traversal. Reach for ripgrep as your default code-search tool; fall back to POSIX grep only when working on systems where rg is not installed or when you need maximum portability in scripts.
Installation
# Debian/Ubuntu
sudo apt install ripgrep
# Fedora/RHEL
sudo dnf install ripgrep
# macOS
brew install ripgrep
# cargo
cargo install ripgrep
Output: (none — exits 0 on success)
Basic usage
rg PATTERN searches every file under the current directory recursively, respecting .gitignore and skipping binary files. Provide a path argument to scope the search; use -e to specify multiple patterns in a single pass.
rg pattern # search cwd recursively
rg pattern file.txt # search a single file
rg pattern src/ tests/ # search specific directories
rg -e pattern1 -e pattern2 # multiple patterns (OR)
Output (rg pattern):
src/auth/login.py
14: if not check_pattern(value):
27: raise ValueError("invalid pattern")
src/utils/validate.py
8: # pattern must match RFC 5321
42: compiled_pattern = re.compile(expr)
Output (rg -e pattern1 -e pattern2):
src/config.py
3:PATTERN1 = r'\d{4}-\d{2}-\d{2}'
17:PATTERN2 = r'[A-Z]{2,5}'
Common flags
| Flag | Meaning |
|---|---|
-i | Case-insensitive |
-v | Invert match |
-n | Line numbers (on by default) |
-l | Filenames only |
-c | Count per file |
-w | Whole word |
-x | Whole line |
-o | Only matching part |
-A N | N lines after |
-B N | N lines before |
-C N | N lines context |
-m N | Max N matches |
-q | Quiet (exit code only) |
--no-filename / -I | Suppress filenames |
File filtering
Narrow the search to specific file types with -g globs or -t type aliases (which map common extensions like py, ts, js). Prefix a glob with ! to exclude. Use --hidden, --no-ignore, or -u/-uu/-uuu to progressively override rg's default exclusions.
rg pattern -g "*.py" # glob: only .py files
rg pattern -g "!*.min.js" # glob: exclude minified
rg pattern -g "src/**/*.ts" # glob: nested path
rg pattern -t py # by type alias
rg pattern -t-py # exclude a type
rg --type-list # list all known type aliases
rg pattern --hidden # include hidden files (.dotfiles)
rg pattern --no-ignore # ignore .gitignore
rg pattern --no-ignore-vcs # ignore only VCS ignores
rg pattern -u # unrestricted (= --no-ignore)
rg pattern -uu # + include hidden files
rg pattern -uuu # + search binary files
Output (rg pattern -t py):
src/auth/login.py
14: if not check_pattern(value):
src/utils/validate.py
42: compiled_pattern = re.compile(expr)
Regex (Rust regex by default)
rg uses the Rust regex crate by default — it guarantees linear-time matching, which is why it is fast, but it does not support lookaheads, lookbehinds, or backreferences. Add -P to switch to the PCRE2 engine and unlock those features at a slight performance cost.
rg '\b\d{4}-\d{2}-\d{2}\b' # date pattern
rg '(?i)error' # inline case-insensitive flag
rg '^import\s' --type ts # TS import lines
# PCRE2 (lookahead, lookbehind, backreferences)
rg -P '(?<=href=")[^"]*' # PCRE2 lookbehind
rg -P '\w+(?=\.log)' # lookahead
Output (rg '\b\d{4}-\d{2}-\d{2}\b'):
logs/deploy.log
3:Deployed on 2026-04-24 at 09:00 UTC
18:Rollback triggered 2026-04-24 at 14:22 UTC
data/events.csv
1:date,event
2:2026-04-01,launch
3:2026-04-15,milestone
Fixed strings
-F treats the pattern as a plain literal string rather than a regex, so special characters like ., *, (, and $ are matched verbatim. Use this when searching for exact code snippets or file content where escaping would be error-prone.
rg -F 'function(x, y)' # treat pattern as literal string
rg -F '$.prototype' # special chars safe
Output: (none — exits 0 on success)
Output formatting
rg groups results by file with headings by default. Use --no-heading for flat file:line:match output compatible with tools like awk. --json emits structured JSON events suitable for machine consumption; --color never is useful when piping to tools that do not strip ANSI codes.
rg pattern --no-heading # flat output, one match per line
rg pattern --heading # group by file (default)
rg pattern --json # machine-readable JSON
rg pattern --color never # disable colour
rg pattern --column # show column number
rg pattern --line-number # redundant (on by default), explicit
rg pattern --pretty # force colour + headings in pipe
Output (rg pattern --no-heading):
src/auth/login.py:14: if not check_pattern(value):
src/auth/login.py:27: raise ValueError("invalid pattern")
src/utils/validate.py:42: compiled_pattern = re.compile(expr)
Output (rg pattern --column):
src/auth/login.py
14:23: if not check_pattern(value):
27:34: raise ValueError("invalid pattern")
Replace (display only — does not modify files)
-r REPLACEMENT prints lines with the matched portion substituted, without touching the source files. Use capture groups ($1, $2) in the replacement string. To actually rewrite files, pipe to sd or combine with perl -i.
rg pattern -r replacement # show lines with substitution applied
rg 'foo(\w+)' -r 'bar$1' # with capture group
Output (rg 'foo(\w+)' -r 'bar$1'):
src/api/routes.py
12: return barhandler(request)
45: cache_key = barprefix + name
Searching compressed files
-z (or --search-zip) decompresses files on the fly before searching, supporting .gz, .bz2, .xz, and .zst. The decompressed content is never written to disk, so it works well for searching large log archives.
rg -z pattern file.gz # decompress on the fly
rg -z pattern logs/ # recursively in .gz/.xz/.bz2
Output: (none — exits 0 on success)
Config file ~/.config/ripgrep/ripgreprc
Persist default flags so you do not have to repeat them on every invocation. rg reads the file pointed to by RIPGREP_CONFIG_PATH; if unset it looks for ~/.config/ripgrep/ripgreprc. Each line is treated as a single flag or argument.
# Ignore node_modules globally
--glob=!node_modules
# Default type filters
--type-add=web:*.{html,css,js,ts,tsx}
# Always show line numbers
--line-number
# Use PCRE2 by default
--engine=pcre2
# Point to a custom config
RIPGREP_CONFIG_PATH=~/.ripgreprc rg pattern
Output: (none — exits 0 on success)
Practical examples
# Find all TODO/FIXME comments in a codebase
rg '(TODO|FIXME|HACK|XXX)' --type py --type ts
Output:
src/auth/login.py
42: # TODO: add rate limiting
src/api/routes.py
118: # FIXME: validate input schema before passing to handler
201: # HACK: workaround for upstream bug #4421
src/cache/redis.py
7: # TODO: implement TTL expiry
# Search for a function definition
rg 'def\s+authenticate' -t py
Output:
src/auth/login.py
15:def authenticate(username, password):
tests/test_auth.py
8:def authenticate(mock_db, username="alice", password="secret"):
# Find files importing a specific module
rg '^import.*from.*lodash' -t ts -l
Output:
src/utils/format.ts
src/components/Table.tsx
src/hooks/useSort.ts
# Extract all email addresses
rg -oP '[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}' --no-filename | sort -u
Output:
admin@example.com
alice@corp.example.com
noreply@service.example.org
support@example.com
# Find duplicate lines within a file
rg '^(.+)$' file.txt --no-filename | sort | uniq -d
Output:
192.168.1.1
error: connection refused
# Count lines matching pattern across all .log files
rg -c 'ERROR' -g "*.log" | awk -F: '{s+=$2} END {print s}'
Output:
312
# Live search a growing log file
tail -f /var/log/syslog | rg --passthru 'error|warn'
# Replace preview (what sed -i would change)
rg 'oldname' -r 'newname' --passthru src/
Output: (none — exits 0 on success)
# rg --stats
rg --stats 'pattern' src/
Output:
src/auth/login.py
14: if not check_pattern(value):
3 matches
2 matched lines
1 files contained matches
24 files searched
0 bytes printed
143029 bytes searched
0.023415 seconds spent searching
0.035801 seconds
# Context lines around a match
rg -A 2 -B 2 'raise ValueError' src/
Output:
src/auth/login.py
25- result = validate(value)
26- if not result:
27: raise ValueError("invalid pattern")
28- return result
29-
src/utils/validate.py
61- parsed = parse(raw)
62- if parsed is None:
63: raise ValueError(f"cannot parse: {raw!r}")
64- return parsed
65-
rg vs grep equivalents
| grep | rg |
|---|---|
grep -r pattern . | rg pattern |
grep -rn pattern . | rg pattern (numbers default) |
grep -rl pattern . | rg -l pattern |
grep -ri pattern . | rg -i pattern |
grep --include="*.py" | rg -t py or rg -g "*.py" |
grep -rP pattern . | rg -P pattern |
Types in depth
A "type" is a named bundle of file globs (e.g. py → *.py, *.pyi, *.pyw). Types are faster to type than globs and survive renames; they also compose well with -T to exclude. Add your own types in ~/.config/ripgrep/ripgreprc so every machine you set up behaves the same.
rg --type-list # show every built-in type
rg --type-list | rg '^(py|ts|js|go|rust):' # show only a few
rg pattern -t py # include type
rg pattern -T py # EXCLUDE type
rg pattern -t py -t pyi # multiple includes (OR)
rg pattern -t web # custom alias defined below
# Define your own type at the CLI
rg --type-add 'web:*.{html,css,js,ts,tsx,jsx,vue,svelte}' -t web 'pattern'
# Or persistently in ripgreprc:
# --type-add=web:*.{html,css,js,ts,tsx,jsx,vue,svelte}
# --type-add=infra:*.{tf,tfvars,yaml,yml,toml,Dockerfile}
Output (rg --type-list | rg '^(py|ts|js):'):
js: *.cjs, *.js, *.mjs, *.vue
py: *.py, *.pyi, *.pyw, Sconstruct
ts: *.cts, *.mts, *.ts, *.tsx
Ignore behaviour and .rgignore
By default rg walks the tree like a respectful guest: it honors .gitignore, .ignore, .rgignore, global gitignore, and $GIT_DIR/info/exclude, and it skips hidden files and dotfiles. Each escalation of -u removes one layer of politeness.
| Default behaviour | Override |
|---|---|
Honor .gitignore | --no-ignore-vcs |
Honor .ignore and .rgignore | --no-ignore-dot |
| Skip hidden files | --hidden |
| Skip binary files | --binary or -uuu |
| All of the above off | -uuu |
.rgignore files use the same syntax as .gitignore but are honored even outside a git repo. They are typically committed when a directory's exclusion is project-specific rather than VCS-related.
# .rgignore — keep generated assets out of search
generated/
*.snap
fixtures/large/
**/*.min.{js,css}
rg pattern # default: respect all ignores, skip hidden
rg pattern --hidden # include hidden files
rg pattern --no-ignore # turn off .gitignore, .ignore, .rgignore
rg pattern -u # same as --no-ignore
rg pattern -uu # + include hidden files (.dotfiles)
rg pattern -uuu # + search binary files
# Selectively ignore just one tree
rg pattern -g '!third_party/'
Output: (none — exits 0 on success)
Globs and path constraints
Globs are matched against each candidate file's path relative to the search root. They support *, **, ?, [abc], brace expansion, and a leading ! to negate. Glob and type flags may be combined freely.
rg pattern -g '*.py' # only Python files
rg pattern -g 'src/**/*.ts' # nested
rg pattern -g '!**/__pycache__/**' # exclude cache dirs
rg pattern -g '**/{Dockerfile,*.dockerfile}' # multiple shapes
rg pattern -g '!*.{min.js,min.css}' # exclude minified
rg pattern --max-depth 2 # cap traversal depth
rg pattern --max-filesize 1M # skip files larger than 1 MB
rg pattern --max-filesize 5K # tiny files only
rg pattern --max-count 3 # stop after 3 matches per file
rg pattern --max-columns 200 # skip lines wider than 200 cols
rg pattern --max-columns-preview # show first 200 cols of long lines
Output (rg pattern -g '!**/__pycache__/**' -g '*.py'):
src/auth/login.py
42: # TODO: add rate limiting
src/utils/cache.py
7: # TODO: implement expiry
Smart case vs explicit case
-S (smart case) is rg's killer default-friendly flag: it is case-insensitive when the pattern is all lowercase and case-sensitive otherwise. Most users add --smart-case to their ripgreprc and forget about -i. Override explicitly with -i (force insensitive) or -s (force sensitive).
rg -S 'error' # case-insensitive (pattern is lowercase)
rg -S 'Error' # case-sensitive (pattern has uppercase)
rg -i 'Error' # force case-insensitive
rg -s 'error' # force case-sensitive
Output: (none — exits 0 on success)
Multiline mode
By default rg matches one line at a time. -U enables multiline mode where the regex can span newlines, and --multiline-dotall makes . also match \n. This is the right tool for finding multi-line patterns like SQL queries broken across lines or HTML elements with attributes on the next line.
# Match a function that takes 3 args spread across lines
rg -U 'def authenticate\(\s*[^,]+,\s*[^,]+,\s*[^)]+\)' src/
# Match a 3-line SQL UPDATE
rg -U --multiline-dotall 'UPDATE\s+users.*?WHERE\s+id\s*=\s*\d+;' migrations/
# Match an opening tag and its closing tag on later line
rg -U --multiline-dotall '<form[^>]*>.*?</form>' templates/
Output (rg -U 'def authenticate\(...\)' src/):
src/auth/login.py
15:def authenticate(
16: username,
17: password,
18: twofa_code,
19:):
Preprocessing (--pre)
--pre runs an arbitrary command on each file and searches the command's stdout instead of the file. Combine with --pre-glob to only preprocess specific extensions. This is how you grep inside PDFs, gzipped tarballs, SQLite databases, or any other format with a CLI extractor.
# Setup: a wrapper that dispatches by extension
cat > ~/.local/bin/rg-preprocess <<'SH'
#!/bin/sh
case "$1" in
*.pdf) pdftotext -layout "$1" - ;;
*.docx) pandoc -t plain "$1" ;;
*.gz|*.tgz) gunzip -c "$1" ;;
*.bz2) bunzip2 -c "$1" ;;
*.zst) zstd -dcq "$1" ;;
*.sqlite|*.db) sqlite3 "$1" .dump ;;
*) cat "$1" ;;
esac
SH
chmod +x ~/.local/bin/rg-preprocess
# Use it
rg 'invoice' --pre rg-preprocess --pre-glob '*.{pdf,docx}' docs/
Output: (none — exits 0 on success)
JSON output and machine consumption
--json emits one JSON object per line for each begin, match, context, end, and summary event. It is the right format when piping into another tool (an editor plugin, a CI report generator, or jq for ad-hoc filtering) because every byte offset, line number, and submatch boundary is structured.
rg --json 'pattern' src/ | head -3
Output:
{"type":"begin","data":{"path":{"text":"src/auth/login.py"}}}
{"type":"match","data":{"path":{"text":"src/auth/login.py"},"lines":{"text":" if not check_pattern(value):\n"},"line_number":14,"absolute_offset":288,"submatches":[{"match":{"text":"pattern"},"start":15,"end":22}]}}
{"type":"end","data":{"path":{"text":"src/auth/login.py"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":31420,"human":"0.000031s"},"searches":1,"searches_with_match":1,"bytes_searched":402,"bytes_printed":54,"matched_lines":1,"matches":1}}}
# Count matches per file using jq
rg --json TODO src/ | jq -r 'select(.type=="end") | "\(.data.stats.matches)\t\(.data.path.text)"'
# Editor plugin pattern: emit (file, line, col, match) tuples
rg --json --column 'pattern' . \
| jq -r 'select(.type=="match") | [.data.path.text, .data.line_number, (.data.submatches[0].start+1), .data.submatches[0].match.text] | @tsv'
Output: (none — exits 0 on success)
Terminal hyperlinks (ripgrep 14+)
--hyperlink-format wraps every printed file path in an OSC 8 terminal-hyperlink escape so a modern terminal can open the file (and jump to the exact line/column) on click. Introduced in ripgrep 14.0 and stabilised through 15.x, it is opt-in today but on track to become opt-out. Pick the format that matches your editor/terminal — default produces file:// URIs that most terminals will hand to the OS, while vscode, jetbrains, and textmate use editor-specific URI schemes that open at the right line.
rg --hyperlink-format=default pattern src/ # file:// URI
rg --hyperlink-format=vscode pattern src/ # vscode://file/...:line:col
rg --hyperlink-format=vscode-insiders pattern .
rg --hyperlink-format=jetbrains pattern src/ # idea://open?file=...&line=...
rg --hyperlink-format=kitty pattern src/ # kitty-specific scheme
rg --hyperlink-format=iterm2 pattern src/ # iTerm2 scheme
rg --hyperlink-format=macvim pattern src/
rg --hyperlink-format=textmate pattern src/
rg --hyperlink-format=none pattern src/ # explicitly disable
# Persist in ripgreprc so every search produces clickable paths
echo '--hyperlink-format=vscode' >> ~/.config/ripgrep/ripgreprc
Output: (visually identical to a normal search; paths become clickable in supporting terminals — Kitty, WezTerm, iTerm2, Ghostty, recent VS Code integrated terminal, GNOME Terminal 3.48+)
Other ripgrep 14/15 additions
A short tour of flags added or changed since the 13.x line. Most are quality-of-life — --no-require-git is the biggest behaviour change for anyone working in non-git projects with .gitignore files (think node_modules checked into a tarball, or a colocated jj checkout).
# --no-require-git: honor .gitignore even outside a git repo
rg --no-require-git pattern path/
# --stop-on-nonmatch: halt as soon as a non-matching line is seen
# (only meaningful for sorted/ordered input)
rg --stop-on-nonmatch '^2026-' access.log
# -d as a short alias for --max-depth (14.0+)
rg -d 2 pattern
# Generate shell completions and the man page on demand (14.0+)
rg --generate=complete-bash > /etc/bash_completion.d/rg
rg --generate=complete-zsh > "${fpath[1]}/_rg"
rg --generate=complete-fish > ~/.config/fish/completions/rg.fish
rg --generate=man > /usr/local/share/man/man1/rg.1
# Jujutsu (.jj) repos are now treated like git repos for ignore discovery (15.0+)
rg pattern jj-checkout/
# -r/--replace now also applies inside --json output (15.0+)
rg --json -r 'NEW' 'OLD' src/ | jq -r 'select(.type=="match") | .data.lines.text'
Output (rg --generate=man | head -3):
.TH "RG" "1"
.SH NAME
\fBrg\fP \- recursively search the current directory for lines matching a pattern
Passthru and streaming
--passthru prints every line of input, highlighting matches but never suppressing non-matching lines. Combined with --block-buffered (or unbuffered output via stdbuf), rg becomes a colorful streaming highlighter for tail -f.
# Highlight ERROR/WARN in a live log
tail -f /var/log/app.log | rg --passthru --line-buffered 'ERROR|WARN'
# Preview a sed-style replace without modifying anything
rg --passthru 'oldname' -r 'newname' src/main.py | less -R
# Use with a continuous stream of journalctl
journalctl -u nginx -f | rg --passthru --line-buffered '5\d\d|error'
Output: (none — exits 0 on success)
Configuration via RIPGREP_CONFIG_PATH
A ripgreprc file is a list of flags, one per line, that rg pretends you passed before everything else. Comment lines start with #. Settings are still overridable on the CLI — for example --no-config disables the file entirely for a single invocation.
# ~/.config/ripgrep/ripgreprc
# Default to smart-case matching
--smart-case
# Always show line numbers and group by file
--heading
# Custom type aliases
--type-add=web:*.{html,css,js,ts,tsx,jsx,vue,svelte}
--type-add=infra:*.{tf,tfvars,yaml,yml,toml,Dockerfile}
--type-add=docs:*.{md,mdx,txt,adoc,rst}
# Universal ignores (in addition to .gitignore)
--glob=!.git/
--glob=!node_modules/
--glob=!**/__pycache__/
--glob=!**/*.min.{js,css}
# PCRE2 on by default — enables \d \w lookaround
--engine=pcre2
# Sensible defaults for binary handling
--max-columns=400
--max-columns-preview
# Tell rg where the config lives (otherwise XDG default)
export RIPGREP_CONFIG_PATH="$HOME/.config/ripgrep/ripgreprc"
# Skip the config for one call
rg --no-config 'pattern'
# Confirm what rg sees as config
rg --debug 'x' 2>&1 | grep -i 'config'
Output: (none — exits 0 on success)
Replace with --passthru and downstream tools
rg does not modify files directly. The common patterns are: preview with --passthru -r, then either (a) pipe to sd or sed -i for the actual write, or (b) use rg -l to list files for a follow-up sed -i.
# 1. Preview the replacement
rg --passthru 'old_api_url' -r 'new_api_url' src/
# 2. Apply only after eyeballing the preview
rg -l 'old_api_url' src/ | xargs sed -i 's|old_api_url|new_api_url|g'
# 3. NUL-safe variant for paths with spaces
rg -l0 'old_api_url' src/ | xargs -0 sed -i 's|old_api_url|new_api_url|g'
# 4. Use sd (Rust replacement for sed -i) when available
rg -l 'old_api_url' src/ | xargs sd 'old_api_url' 'new_api_url'
Output: (none — exits 0 on success)
fzf integration
Piping rg into fzf produces an interactive picker over every matching line in a codebase — usually bound to a key in your shell or editor. The classic "ripgrep + fzf + bat preview" combo gives you Sublime-Text-style "Find in Project" from any terminal. See the fzf page in this section for the full picker setup.
# Quick: pick a match and open it in $EDITOR at the right line
rg --line-number --no-heading --color=always 'pattern' \
| fzf --ansi --delimiter ':' \
--preview 'bat --color=always --highlight-line {2} {1}' \
--bind 'enter:execute($EDITOR +{2} {1})'
Output: (none — exits 0 on success)
Comparison: rg vs ag vs grep vs ugrep vs ast-grep
The Silver Searcher (ag) was the previous-generation "smart grep" — it pioneered respecting .gitignore by default. rg is its spiritual successor with a faster regex engine, better Unicode support, and active maintenance. Two newer entrants attack different parts of the problem: ugrep is a GNU-grep-compatible drop-in with Google-style boolean queries, an interactive TUI, and native search inside zip/7z/tar archives and PDFs; ast-grep trades regex for tree-sitter ASTs, so patterns match syntactic structures (function calls, class declarations) rather than character sequences. Reach for ugrep when you want grep-flag muscle memory plus archive/PDF support, and for ast-grep when you need precise structural search-and-replace across a codebase.
| Feature | grep | ag | rg | ugrep | ast-grep |
|---|---|---|---|---|---|
| Default recursion | needs -r | yes | yes | needs -r | yes |
Honors .gitignore | no | yes | yes | opt-in (--ignore-files) | yes |
| Skips hidden | no | yes | yes | configurable | yes |
| Line numbers default | no | yes | yes | yes | yes |
| Smart case | no | yes | -S | -j | n/a |
| Multi-line regex | -z trick | limited | -U | yes | inherent |
| PCRE / lookaround | -P | partial | -P | -P | n/a |
| Boolean AND/OR/NOT | no | no | no (pipe stages) | --bool | composable rules |
| Search archives (zip/7z) | no | no | .gz/.bz2/.xz/.zst via -z | yes (nested) | no |
| Search PDFs / docx | no | no | --pre only | built-in | no |
| Interactive TUI | no | no | no | ug --query | no |
| Replacement preview | no | no | --passthru -r | --replace | structural rewrite |
| Replacement writes files | -i (GNU) | no | no (pipe to sd) | yes | yes (--update-all) |
| Syntax-aware (AST) | no | no | no | no | yes |
| Terminal hyperlinks | no | no | --hyperlink-format (14+) | --hyperlink | yes |
| Speed on large repos | baseline | ~2–5× | ~5–20× | comparable to rg | slower (AST cost) |
| Active development | maintenance | low | active | active | active |
Workflow recipes
# 1. Search only Rust source (skip target/ automatically via .gitignore)
rg 'unsafe' -t rust
# 2. Codebase-wide TODO census with counts per file
rg -c '(TODO|FIXME|HACK|XXX)' --type-add 'src:*.{py,ts,js,go,rs}' -t src \
| sort -t: -k2 -rn
# 3. Find the file that defines a symbol, then open it
file=$(rg -l --type ts '^export (function|class|const) handleAuth' | head -1)
[ -n "$file" ] && $EDITOR "$file"
# 4. Compare two branches: lines added that contain TODO
git diff main...HEAD -- '*.py' | rg '^\+.*TODO' --color=always
# 5. Stat-mode: how big is the search surface?
rg --files | wc -l # files rg would search
rg --files | xargs wc -l | tail -1 # total searchable lines
# 6. Cross-reference two patterns (files that match BOTH)
comm -12 <(rg -l 'pattern_a' | sort) <(rg -l 'pattern_b' | sort)
# 7. Extract structured data with PCRE2 + lookbehind
rg -PNo --no-filename '(?<=version\s=\s")[^"]+' Cargo.toml */Cargo.toml | sort -u
# 8. Find binaries containing a string (force binary search)
rg --text 'libssl' /usr/lib/
# 9. Hex search in binaries
rg --multiline -aP '\x00\x01\x02\x03' firmware.bin
# 10. Print only the matched part, deduplicated
rg -o --no-filename '[A-Z][A-Z0-9_]+_TOKEN' src/ | sort -u
Output (rg -c '(TODO|FIXME|HACK|XXX)' ... | sort -t: -k2 -rn):
src/api/routes.py:12
src/auth/login.py:7
src/utils/cache.py:4
src/components/Table.tsx:3
src/main.py:1
Common pitfalls
- Hidden files are invisible by default: rg skips dotfiles even outside git repos. Add
--hiddenor set-uuto include them. .gitignoreoutside a repo is ignored: rg only honors.gitignoreinside a git checkout. Use.ignoreor.rgignorefor non-git projects.- Lookaround needs
-P: the default Rust regex engine has no lookarounds or backreferences. Add-Pto switch to PCRE2, or set--engine=pcre2inripgreprc. rg pattern *.txtexpands the glob in your shell: preferrg pattern -g '*.txt'. Otherwise rg sees only the files the shell expanded — recursion is lost.- Smart-case surprises:
rg -S Errorwon't matcherror. If you want case-insensitive regardless of pattern case, use-i. - Output looks different in pipelines: rg drops the headings/colors when stdout is not a TTY. Force them back with
--heading --color always(and pair withless -R). - Replacement only writes to stdout:
rg -rpreviews a replace; nothing is written to disk. Pipe tosed -iorsdto commit changes.
Tips
rg --fileslists every file rg would search (respecting ignores) without actually searching. Useful for debugging why a file is included or excluded.
Set
--engine=pcre2in yourripgreprcto make\d,\w, and lookarounds available everywhere without having to remember-Pon every invocation.
Combine
--jsonwithjqfor ad-hoc analytics — for example, top files by match count or aggregating submatch text across a tree.
[!WARN]
-uuusearches binary files. It can produce gigantic, garbled output on a repo containing images, compiled artefacts, or media. Start with-uu(just include hidden) and only escalate if you specifically need to search binaries.