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

bash
# 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.

bash
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):

text
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):

text
src/config.py
3:PATTERN1 = r'\d{4}-\d{2}-\d{2}'
17:PATTERN2 = r'[A-Z]{2,5}'

Common flags

FlagMeaning
-iCase-insensitive
-vInvert match
-nLine numbers (on by default)
-lFilenames only
-cCount per file
-wWhole word
-xWhole line
-oOnly matching part
-A NN lines after
-B NN lines before
-C NN lines context
-m NMax N matches
-qQuiet (exit code only)
--no-filename / -ISuppress 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.

bash
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):

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

bash
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'):

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

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

bash
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):

text
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):

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

bash
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'):

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

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

text
# 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
bash
# Point to a custom config
RIPGREP_CONFIG_PATH=~/.ripgreprc rg pattern

Output: (none — exits 0 on success)

Practical examples

bash
# Find all TODO/FIXME comments in a codebase
rg '(TODO|FIXME|HACK|XXX)' --type py --type ts

Output:

text
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
bash
# Search for a function definition
rg 'def\s+authenticate' -t py

Output:

text
src/auth/login.py
15:def authenticate(username, password):

tests/test_auth.py
8:def authenticate(mock_db, username="alice", password="secret"):
bash
# Find files importing a specific module
rg '^import.*from.*lodash' -t ts -l

Output:

text
src/utils/format.ts
src/components/Table.tsx
src/hooks/useSort.ts
bash
# Extract all email addresses
rg -oP '[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}' --no-filename | sort -u

Output:

text
admin@example.com
alice@corp.example.com
noreply@service.example.org
support@example.com
bash
# Find duplicate lines within a file
rg '^(.+)$' file.txt --no-filename | sort | uniq -d

Output:

text
192.168.1.1
error: connection refused
bash
# Count lines matching pattern across all .log files
rg -c 'ERROR' -g "*.log" | awk -F: '{s+=$2} END {print s}'

Output:

text
312
bash
# 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)

bash
# rg --stats
rg --stats 'pattern' src/

Output:

text
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
bash
# Context lines around a match
rg -A 2 -B 2 'raise ValueError' src/

Output:

text
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

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

bash
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):'):

text
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 behaviourOverride
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.

text
# .rgignore — keep generated assets out of search
generated/
*.snap
fixtures/large/
**/*.min.{js,css}
bash
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.

bash
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'):

text
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).

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

bash
# 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/):

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

bash
# 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.

bash
rg --json 'pattern' src/ | head -3

Output:

text
{"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}}}
bash
# 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)

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

bash
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).

bash
# --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):

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

bash
# 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.

text
# ~/.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
bash
# 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.

bash
# 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.

bash
# 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.

Featuregrepagrgugrepast-grep
Default recursionneeds -ryesyesneeds -ryes
Honors .gitignorenoyesyesopt-in (--ignore-files)yes
Skips hiddennoyesyesconfigurableyes
Line numbers defaultnoyesyesyesyes
Smart casenoyes-S-jn/a
Multi-line regex-z tricklimited-Uyesinherent
PCRE / lookaround-Ppartial-P-Pn/a
Boolean AND/OR/NOTnonono (pipe stages)--boolcomposable rules
Search archives (zip/7z)nono.gz/.bz2/.xz/.zst via -zyes (nested)no
Search PDFs / docxnono--pre onlybuilt-inno
Interactive TUInononoug --queryno
Replacement previewnono--passthru -r--replacestructural rewrite
Replacement writes files-i (GNU)nono (pipe to sd)yesyes (--update-all)
Syntax-aware (AST)nonononoyes
Terminal hyperlinksnono--hyperlink-format (14+)--hyperlinkyes
Speed on large reposbaseline~2–5×~5–20×comparable to rgslower (AST cost)
Active developmentmaintenancelowactiveactiveactive

Workflow recipes

bash
# 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):

text
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 --hidden or set -uu to include them.
  • .gitignore outside a repo is ignored: rg only honors .gitignore inside a git checkout. Use .ignore or .rgignore for non-git projects.
  • Lookaround needs -P: the default Rust regex engine has no lookarounds or backreferences. Add -P to switch to PCRE2, or set --engine=pcre2 in ripgreprc.
  • rg pattern *.txt expands the glob in your shell: prefer rg pattern -g '*.txt'. Otherwise rg sees only the files the shell expanded — recursion is lost.
  • Smart-case surprises: rg -S Error won't match error. 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 with less -R).
  • Replacement only writes to stdout: rg -r previews a replace; nothing is written to disk. Pipe to sed -i or sd to commit changes.

Tips

rg --files lists every file rg would search (respecting ignores) without actually searching. Useful for debugging why a file is included or excluded.

Set --engine=pcre2 in your ripgreprc to make \d, \w, and lookarounds available everywhere without having to remember -P on every invocation.

Combine --json with jq for ad-hoc analytics — for example, top files by match count or aggregating submatch text across a tree.

[!WARN] -uuu searches 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.

Sources