Replacements¶
Ripgrep can replace matched text in its output using the -r/--replace flag. This is useful for reformatting search results, extracting parts of matches, or transforming text patterns.
File Safety
Replacements only affect ripgrep's output and never modify your files. Your source files remain completely unchanged.
Quick Reference
How Replacements Work¶
When you use the -r/--replace flag, ripgrep processes text through several stages:
Implementation Details
The replacement logic is implemented in crates/matcher/src/interpolate.rs, which handles capture group interpolation and special character escaping. The printer configuration is in crates/printer/src/standard.rs.
flowchart LR
Input[Input] --> Match{Matches?}
Match -->|No| Skip[Skip]
Match -->|Yes| Extract[Extract Groups]
Extract --> Replace[Apply Replace]
Replace --> Output[Output]
Skip --> Output
style Input fill:#e1f5ff
style Extract fill:#fff3e0
style Replace fill:#e8f5e9
style Output fill:#f3e5f5
Figure: Replacement processing flow showing how matches are transformed into output.
The Replace Flag¶
The basic syntax is:
The -r flag (or --replace) tells ripgrep to replace each match with the specified replacement text in the output. For example:
# Replace "foo" with "bar" in output
rg foo -r bar
# Search for "error" but display "ERROR" in results
rg error -r ERROR
Remember: this modifies what ripgrep prints, not the files themselves. Your files remain unchanged.
Capture Groups¶
The real power of replacements comes from capture groups, which let you reference parts of the matched text in your replacement string.
Numbered Capture Groups¶
Capture groups in your regex pattern are numbered based on the position of their opening parenthesis, starting from 1. The special group $0 represents the entire match.
# Swap two words
rg '(\w+) (\w+)' -r '$2 $1'
# Extract area code from phone numbers
rg '(\d{3})-(\d{3})-(\d{4})' -r 'Area code: $1'
In the first example:
- (\w+) is capture group $1 (first word)
- (\w+) is capture group $2 (second word)
- The replacement '$2 $1' swaps them
Named Capture Groups¶
You can also use named capture groups for more readable patterns. The syntax is (?P<name>pattern) in Rust regex. The alternative syntax (?<name>pattern) is also supported in both Rust regex and PCRE2 mode.
# Using named groups for clarity
rg '(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})' \ # (1)!
-r 'Year: ${year}, Month: ${month}' # (2)!
# Extract protocol and domain from URLs
rg '(?P<protocol>https?)://(?P<domain>[^/]+)' \ # (3)!
-r '$protocol at $domain' # (4)!
- Named capture groups with
(?P<name>pattern)syntax make complex patterns self-documenting - Reference named groups with
$nameor${name}(braces required when followed by text) - The
?makes thesinhttps?optional, matching bothhttpandhttps - Can reference named groups without braces if not followed by valid name characters
Named groups make complex patterns more maintainable and self-documenting.
Capture Group Syntax Rules¶
Understanding Capture Group References
Understanding how ripgrep parses capture group references is important. The parsing logic is implemented in crates/matcher/src/interpolate.rs.
- Valid characters: Group names consist of letters, numbers, and underscores only (
[_0-9A-Za-z]) - Longest match: Ripgrep takes the longest matching name after
$ $1arefers to a group named "1a", not group 1 followed by "a"- Use braces to separate:
${1}ameans group 1 followed by "a" - Brace syntax: Use
${name}or${1}to disambiguate from following text - Invalid references: If a group doesn't exist, it's replaced with an empty string
- Literal dollar sign: Use
$$to write a literal$
flowchart TD
Start["$1a"] --> Parse{Has Braces?}
Parse -->|No| Longest["Parse Longest
Valid Name"]
Parse -->|"Yes ${1}a"| Extract["Extract Group
Reference"]
Longest --> Check{"Group 1a
Exists?"}
Extract --> Check2{"Group 1
Exists?"}
Check -->|Yes| Output1["Value of group 1a"]
Check -->|No| Output2[Empty String]
Check2 -->|Yes| Output3["Value of group 1
+ literal a"]
Check2 -->|No| Output4["Empty + literal a"]
style Parse fill:#e1f5ff
style Longest fill:#fff3e0
style Extract fill:#e8f5e9
style Output1 fill:#f3e5f5
style Output3 fill:#f3e5f5
Figure: How ripgrep parses capture group references with and without braces.
Examples:
# Without braces - "1a" is interpreted as a group name
rg '(\w+)' -r '$1a' # Looks for group named "1a"
# With braces - separate group reference from text
rg '(\w+)' -r '${1}a' # Group 1 followed by "a"
# Literal dollar sign
rg 'price' -r '$$5.00' # Outputs "$5.00"
Shell Quoting¶
Critical: Always Use Single Quotes
In shells like Bash and zsh, always use single quotes for the replacement string to prevent shell variable expansion.
# Wrong - shell expands $1 (usually to empty string)
rg '(\w+)' -r "$1"
# Correct - single quotes prevent shell expansion
rg '(\w+)' -r '$1'
Without proper quoting, $1 gets replaced by a shell variable (which is likely undefined and empty) before ripgrep even sees it.
Output Modification Behavior¶
By default, replacements work on each individual match, not entire lines:
# Replaces only the matched pattern, not the whole line
rg 'foo' -r 'bar'
# Line: "foo and foo" becomes "bar and bar"
To replace entire lines, match the entire line:
Using with Other Flags¶
Replacements work well with other ripgrep flags:
With --only-matching (-o): Extract and transform parts of matches
# Extract and reformat email addresses
rg '(\w+)@(\w+\.com)' -o -r '$1 at $2'
# Input: "Contact: john@example.com"
# Output: "john at example.com"
With context flags (-A, -B, -C): Replacements only apply to matched lines, not context lines
# Replace matches but keep context unchanged
rg 'error' -r 'ERROR' -C1
# If input is:
# info message
# error occurred
# debug info
# Output shows:
# info message (context - unchanged)
# ERROR occurred (match - replaced)
# debug info (context - unchanged)
With --json: The JSON output includes a submatches array with replacement text in the match field
# Example JSON output with replacements
rg 'error' -r 'ERROR' --json
# Produces output like:
# {
# "type": "match",
# "data": {
# "submatches": [
# {
# "match": {"text": "ERROR"},
# "start": 10,
# "end": 15
# }
# ]
# }
# }
Practical Examples¶
Simple Literal Replacement¶
Swapping Words¶
# Reverse first and last names
# Source: tests/misc.rs:199-209 (replace_groups test)
rg '([A-Z][a-z]+) ([A-Z][a-z]+)' -r '$2, $1'
# "John Watson" becomes "Watson, John"
Extracting Data¶
# Extract just the path from log entries
# Source: tests/misc.rs:228-240 (replace_with_only_matching test)
rg 'GET (/[^\s]+)' -r '$1' -o
# Input: "GET /api/users HTTP/1.1"
# Output: "/api/users"
Reformatting Dates¶
# Convert YYYY-MM-DD to Month DD, YYYY
rg '(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})' \
-r '${month}/${day}/${year}'
Multiple Matches Per Line¶
# Replace all occurrences of a pattern in each line
rg 'TODO' -r 'DONE'
# "TODO: Fix TODO items" becomes "DONE: Fix DONE items"
Removing Matches¶
Use an empty replacement string to remove matches from output:
Edge Cases and Special Behaviors¶
Invalid Capture Groups¶
References to non-existent groups are replaced with empty strings:
# Pattern has only 1 group, but replacement uses $2
rg '(\w+)' -r '$1 and $2'
# $2 is replaced with empty string
Empty Replacements¶
An empty replacement string removes matches:
Multiline Mode¶
Replacements work with multiline mode (-U):
Maximum Column Limits¶
When using --max-columns, replacements are applied before the column limit check:
# Replacement happens first, then column limit applies
rg 'short' -r 'very_long_replacement' --max-columns 50
Comparison: Numbered vs Named Groups¶
| Feature | Numbered ($1, $2) |
Named ($name) |
|---|---|---|
| Syntax | (\w+) → $1 |
(?P<name>\w+) → $name |
| Readability | Requires counting | Self-documenting |
| Maintenance | Fragile if pattern changes | More robust |
| Best for | Simple patterns | Complex patterns |
Example showing both:
Common Mistakes and Troubleshooting¶
Forgot Shell Quoting¶
# Problem: Shell expands $1 before ripgrep sees it
rg '(\w+)' -r "$1"
# Solution: Use single quotes
rg '(\w+)' -r '$1'
Wrong Group Numbers with Nested Groups¶
# Groups are numbered by opening parenthesis position
rg '((\w+) (\w+))' -r '$1 / $2 / $3'
# $1 = entire match (both words)
# $2 = first word
# $3 = second word
Name Parsing Issues¶
# Ambiguous: is this group "1a" or group 1 + "a"?
rg '(\w+)' -r '$1a' # Looks for group named "1a"
# Unambiguous with braces
rg '(\w+)' -r '${1}a' # Group 1 followed by "a"
Real-World Use Cases¶
Practical Applications
These examples show how replacements solve common text processing tasks. All transformations happen in ripgrep's output without modifying source files.
Reformatting log entries:
# Convert Apache logs to simplified format
rg '(\S+) - - \[([^\]]+)\] "GET (\S+)"' \ # (1)!
-r 'IP: $1 | Time: $2 | Path: $3' # (2)!
- Pattern captures IP address, timestamp, and request path from Apache log format
- Reformats into pipe-delimited format for easier parsing
Extracting structured data:
Sanitizing output:
Data transformation pipelines:
Performance Considerations¶
Performance Impact
Replacements require extracting capture groups, which adds some overhead compared to simple matching. However, this overhead is generally negligible for most use cases. Ripgrep's implementation amortizes allocations across matches for efficiency.
For performance-critical scenarios, consider:
- Using simpler patterns when possible
- Avoiding unnecessary capture groups
- Using
--only-matchingto reduce output volume
Reference: Replacement Syntax¶
| Syntax | Meaning | Example |
|---|---|---|
$0 |
Entire match | rg 'foo' -r '$0$0' → "foofoo" |
$1, $2, ... |
Numbered groups | rg '(\w+)' -r '$1' |
$name |
Named group | rg '(?P<x>\w+)' -r '$x' |
${1}, ${name} |
Braced reference | rg '(\w+)' -r '${1}!' |
$$ |
Literal $ |
rg 'price' -r '$$5' → "$5" |
See Also¶
- Output Formats - For other output formatting options
- Introduction - For getting started with ripgrep
- Recursive Search - For file traversal and pattern matching basics