How ripgrep's Replacement Feature Works: A Deep Dive into --replace

ripgrep's --replace flag performs display-only text substitution during search, interpolating capture groups like $1 and ${name} into the output without ever modifying source files.

The BurntSushi/ripgrep repository provides a blazingly fast search tool that includes a powerful ripgrep replacement feature via the -r or --replace flag. Unlike traditional sed-style replacements, this feature is strictly display-only—it transforms how matches appear in your terminal without altering the underlying files. Understanding how this substitution pipeline works reveals why ripgrep can perform complex text transformations while maintaining its characteristic speed.

How ripgrep Replacement Works Under the Hood

The implementation follows a three-stage pipeline that parses the replacement template, prepares efficient buffers, and executes the substitution for every matching line.

Step 1: Parsing the --replace Flag

When you invoke -r or --replace, the flag definition in crates/core/flags/defs.rs handles parsing. The Replace struct stores the replacement template as a BString (byte string) in LowArgs::replace, preserving raw bytes to support searching non-UTF-8 data.

Step 2: Initializing the Replacer

During result printing, the printer lazily creates a Replacer<M> struct defined in crates/printer/src/util.rs. This structure holds a reusable Space<M> allocation that stores:

  • Capture group locations from the regex matcher
  • A destination buffer for the interpolated output
  • The new match offsets after replacement

By reusing these allocations across lines, ripgrep avoids per-line memory allocation overhead.

Step 3: Executing the Substitution

For every line output—whether a match or a context line—the sink invokes StandardSink::replace in crates/printer/src/standard.rs. If replacement is enabled, this calls Replacer::replace_all from crates/printer/src/util.rs, which:

  1. Detects whether the search pattern is multiline
  2. Strips line terminators for single-line searches to ensure look-around assertions work correctly
  3. Invokes replace_with_captures_in_context to interpolate $1, ${foo}, and $$ using the matcher's capture data
  4. Stores the transformed bytes in Space::dst and records the new match ranges for highlighting

ripgrep Replacement Syntax and Capture Groups

The replacement string supports several interpolation patterns that reference captured portions of the match.

Numeric capture groups use $n syntax where n is the index of the capturing parenthesis (counting from the left-most opening parenthesis). $0 expands to the entire match.

Named capture groups use $name syntax. If the name contains characters outside of letters, digits, or underscores, or if you need to disambiguate from surrounding text, use the braced form ${name}.

Escaping dollar signs requires doubling them: $$ produces a literal $ character in the output.

When writing these patterns in shell commands, use single quotes to prevent the shell from interpreting $1 as a variable. For example, write rg --replace '$1' 'pattern' rather than using double quotes.

Practical Examples of ripgrep --replace

Simple String Replacement

Replace occurrences of "error" with "warning" in the src/ directory:

rg --replace 'warning' 'error' src/

Swapping Capture Groups

Rearrange CSV fields by swapping the first and second columns:

rg --replace '$2,$1' '^([^,]+),([^,]+)' data.csv

This transforms "first,second" into "second,first".

Named Capture Groups

Reformat ISO dates to European format using named groups:

rg --replace '${day}/${month}/${year}' \
   '(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})' logs.txt

Literal Dollar Signs

Insert a literal dollar sign before prices:

rg --replace '$$${0}' '\d+' prices.txt

This converts "100" to "$100". The $$ becomes a single $, and ${0} inserts the matched number.

Deleting Lines

Remove lines containing "TODO" by replacing the entire line with an empty string:

rg --replace '' '^.*TODO.*$' source.rs

Key Source Files for ripgrep Replacement

The implementation spans the core flags and printer crates:

File Purpose
crates/core/flags/defs.rs Defines the Replace flag and parses the replacement template into LowArgs::replace as a BString.
crates/printer/src/util.rs Contains the Replacer<M> struct, Space<M> allocation management, and the replace_all method that performs capture interpolation.
crates/printer/src/standard.rs Implements StandardSink::replace, which invokes the replacer for every match and context line during output formatting.
tests/feature.rs Integration tests verifying replacement behavior, including capture group handling and edge cases.

Summary

  • Display-only operation: ripgrep's --replace flag transforms output for terminal display and never modifies source files on disk.
  • Three-stage pipeline: The feature parses the replacement template in defs.rs, initializes a reusable Replacer<M> struct from util.rs, and executes substitutions via StandardSink::replace in standard.rs.
  • Capture group support: The replacement string supports numeric indices ($1, $0), named groups ($name, ${name}), and escaped dollars ($$).
  • Context awareness: Replacements apply to both matching lines and context lines when using -A, -B, or -C flags.
  • Performance optimized: The implementation reuses allocations through Space<M> to avoid per-line memory overhead during large searches.

Frequently Asked Questions

Does ripgrep's --replace modify files in place?

No. The --replace flag is strictly a display feature that transforms how matches appear in the terminal output. It never writes changes back to the source files. If you need to modify files in place, you should pipe ripgrep's output to a tool like sed or use a dedicated file modification utility.

How do I use named capture groups with ripgrep --replace?

You can reference named capture groups using the $name syntax or the ${name} syntax for disambiguation. For example, if your pattern includes (?P<year>\d{4}), you can use --replace '${year}' to insert the captured value. The braces are required if the name is followed immediately by other characters that could be interpreted as part of the name.

Why does my shell break the $1 syntax in ripgrep replacements?

Shells like Bash and Zsh interpret $1 as a shell variable before ripgrep ever sees it. Since these variables are typically unset, they expand to empty strings, breaking your replacement pattern. Always wrap your replacement argument in single quotes, such as --replace '$1 $2', to prevent the shell from expanding the dollar signs.

Can I use ripgrep replacement with multiline patterns?

Yes. The replacement engine in crates/printer/src/util.rs detects multiline searches and handles them appropriately. For single-line searches, it strips line terminators to ensure look-around assertions work correctly, then performs the replacement. For multiline patterns, the replacement applies across the full matched text, preserving the ability to use capture groups that span multiple lines.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →