Skip to content

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

# Basic replacement
rg PATTERN -r REPLACEMENT

# Numbered capture groups
rg '(\w+) (\w+)' -r '$2 $1'

# Named capture groups
rg '(?P<name>\w+)' -r 'Hello $name'

# Always use single quotes!
rg '(\w+)' -r '$1'  # ✓ Correct
rg '(\w+)' -r "$1"  # ✗ Wrong

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:

Basic replacement syntax
rg PATTERN -r REPLACEMENT

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)!
  1. Named capture groups with (?P<name>pattern) syntax make complex patterns self-documenting
  2. Reference named groups with $name or ${name} (braces required when followed by text)
  3. The ? makes the s in https? optional, matching both http and https
  4. 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 $
  • $1a refers to a group named "1a", not group 1 followed by "a"
  • Use braces to separate: ${1}a means 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:

# Match and replace entire line
rg '.*error.*' -r 'REDACTED'

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

# Replace all instances of old API endpoint
rg 'api.old.com' -r 'api.new.com'

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:

# Remove all email addresses from output
rg '\S+@\S+' -r ''

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:

rg 'pattern' -r ''

Multiline Mode

Replacements work with multiline mode (-U):

# Match and replace across lines
rg -U 'foo\nbar' -r 'baz'

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:

# Numbered groups - simple but requires counting
rg '(\d{4})-(\d{2})-(\d{2})' -r '$1/$2/$3'

Good for simple patterns with few groups.

# Named groups - clearer intent
rg '(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})' \
   -r '$year/$month/$day'

Better for complex patterns where clarity matters.

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)!

  1. Pattern captures IP address, timestamp, and request path from Apache log format
  2. Reformats into pipe-delimited format for easier parsing

Extracting structured data:

# Pull out just the error codes
rg 'error_code=(\d+)' -r '$1' -o

Sanitizing output:

# Redact sensitive data
rg 'password=\S+' -r 'password=***REDACTED***'

Data transformation pipelines:

# Chain with other tools for complex transforms
rg '(\w+),(\w+)' -r '$2 $1' -o | sort | uniq

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-matching to 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