How does regex::Regex::replace_all handle overlapping matches compared to replace for single replacements?
replace_all replaces every non-overlapping match in the input string while replace only replaces the first match. Both methods handle overlapping matches the same way—neither can match overlapping patterns because after a match is found and replaced, the search continues from the end of that match, not from the next character. The key difference is simply that replace_all continues searching for additional matches after the first replacement.
The Two Methods
use regex::Regex;
fn basic_difference() {
let re = Regex::new(r"\d+").unwrap();
let text = "a1b2c3d";
// replace: Only first match
let result = re.replace(text, "X");
assert_eq!(result, "aXb2c3d");
// replace_all: All matches
let result = re.replace_all(text, "X");
assert_eq!(result, "aXbXcXd");
}replace stops after the first match; replace_all continues for all matches.
Single Match Behavior
use regex::Regex;
fn single_match() {
let re = Regex::new(r"test").unwrap();
let text = "this is a test and another test";
// replace: First match only
let result = re.replace(text, "EXAM");
assert_eq!(result, "this is a EXAM and another test");
// replace_all: All matches
let result = re.replace_all(text, "EXAM");
assert_eq!(result, "this is a EXAM and another EXAM");
}When there are multiple matches, replace only handles the first one.
Overlapping Matches: Neither Method Supports Them
use regex::Regex;
fn overlapping_limitation() {
// Consider trying to match "aba" in "ababa"
// The matches would overlap: "aba"ba and "ab"aba"
let re = Regex::new(r"aba").unwrap();
let text = "ababa";
// replace: First match only
let result = re.replace(text, "X");
assert_eq!(result, "Xba"); // Only first "aba" replaced
// replace_all: Still only one match found
let result = re.replace_all(text, "X");
assert_eq!(result, "Xba"); // Same result!
// Why? After matching positions 0-3 ("aba"),
// the search continues from position 3
// It never sees the "aba" at positions 2-5
}Neither method finds overlapping matches because regex engines search left-to-right and continue from the end of each match.
How the Regex Engine Searches
use regex::Regex;
fn search_mechanism() {
// The regex engine searches like this:
// 1. Start at position 0
// 2. Try to match pattern at current position
// 3. If match found:
// - For replace: done
// - For replace_all: continue from END of match
// 4. If no match: advance position by 1, go to step 2
let re = Regex::new(r"aa").unwrap();
let text = "aaa";
// Positions: 0 1 2
// Text: a a a
// First match: "aa" at positions 0-1
// After this match:
// - replace: stops
// - replace_all: continues from position 2
// At position 2: "a" doesn't match "aa"
// No more matches
let single = re.replace(text, "X");
assert_eq!(single, "Xa");
let all = re.replace_all(text, "X");
assert_eq!(all, "Xa"); // Same result!
}The search continues from the end of each match, not overlapping with previous matches.
The Key Difference: Continuation
use regex::Regex;
fn continuation_difference() {
let re = Regex::new(r"[a-z]+").unwrap();
let text = "abc123def456ghi";
// replace: First match, then stop
let result = re.replace(text, "X");
assert_eq!(result, "X123def456ghi");
// replace_all: First match, then continue searching
let result = re.replace_all(text, "X");
assert_eq!(result, "X123X456X");
// The process for replace_all:
// 1. Match "abc" at 0-3, replace -> "X"
// 2. Continue from position 3 (after "abc")
// 3. "123" doesn't match, skip
// 4. Match "def" at 6-9, replace -> "X"
// 5. Continue from position 9
// 6. "456" doesn't match, skip
// 7. Match "ghi" at 12-15, replace -> "X"
// Result: "X123X456X"
}replace_all continues searching after each match; replace stops after the first.
Visualizing Non-Overlapping Behavior
use regex::Regex;
fn visualize_matching() {
let re = Regex::new(r"aba").unwrap();
let text = "ababa";
// Text positions: a b a b a
// 0 1 2 3 4
// Match attempt at position 0:
// "aba" matches! (positions 0-2)
// After match at 0-2:
// - replace: stop, return result
// - replace_all: continue from position 3
// Match attempt at position 3:
// "ba" doesn't match "aba"
// The "aba" at positions 2-4 is MISSED
// because search continued from position 3
// Both methods miss the overlapping match
assert_eq!(re.replace(text, "X"), "Xba");
assert_eq!(re.replace_all(text, "X"), "Xba");
}The overlapping match at positions 2-4 is never found because the search continues from position 3.
Capturing Groups with Both Methods
use regex::Regex;
fn capturing_groups() {
let re = Regex::new(r"(\w+)@(\w+)").unwrap();
let text = "foo@bar baz@qux";
// replace: First match only
let result = re.replace(text, "$2.$1");
assert_eq!(result, "bar.foo baz@qux");
// replace_all: All matches
let result = re.replace_all(text, "$2.$1");
assert_eq!(result, "bar.foo qux.baz");
// Capturing groups work with both methods
// Only difference is how many matches are processed
}Both methods support capturing group references; the difference is in match count.
Replacement Callbacks
use regex::Regex;
use regex::Captures;
fn replacement_callbacks() {
let re = Regex::new(r"\d+").unwrap();
let text = "1 2 3 4 5";
// replace with callback: First match only
let result = re.replace(text, |caps: &Captures| {
let num: i32 = caps[0].parse().unwrap();
format!("{}", num * 2)
});
assert_eq!(result, "2 2 3 4 5");
// replace_all with callback: All matches
let result = re.replace_all(text, |caps: &Captures| {
let num: i32 = caps[0].parse().unwrap();
format!("{}", num * 2)
});
assert_eq!(result, "2 4 6 8 10");
// Callbacks receive each match in order
}Both methods support callback-based replacements; replace_all calls the callback for each match.
When Overlapping Matters
use regex::Regex;
fn overlapping_problem() {
// Problem: Find all overlapping matches
// Example: Find all substrings of length 2 that are "aa"
let text = "aaaa";
let re = Regex::new(r"aa").unwrap();
// replace_all finds: "aa" at 0-1 and "aa" at 2-3
let result = re.replace_all(text, "X");
assert_eq!(result, "XX"); // "aa" -> "X", "aa" -> "X"
// But we want to find ALL occurrences including overlapping:
// Position 0-1: "aa"
// Position 1-2: "aa" (overlapping!)
// Position 2-3: "aa"
// Standard regex cannot find these overlapping matches
// You need a different approach
}Standard regex cannot find overlapping matches; alternative approaches are needed.
Handling Overlapping Matches Manually
use regex::Regex;
fn find_overlapping() {
let text = "aaaa";
let re = Regex::new(r"aa").unwrap();
// To find overlapping matches, iterate through all positions
let mut overlapping_matches = Vec::new();
for i in 0..text.len() {
if let Some(m) = re.find_at(text, i) {
if m.start() == i { // Match starts at position i
overlapping_matches.push((m.start(), m.end()));
}
}
}
// This finds: (0, 2), (1, 3), (2, 4)
// All three overlapping "aa" matches
// But replace doesn't support this pattern
// You'd need custom replacement logic
}Use find_at to manually find overlapping matches, but there's no built-in overlapping replacement.
Zero-Width Matches
use regex::Regex;
fn zero_width_matches() {
// Zero-width matches can appear to overlap
// But the engine handles them specially
let re = Regex::new(r"\b").unwrap(); // Word boundary
let text = "a b c";
// Word boundaries are zero-width
// Positions: |a| |b| |c|
// (where | is a boundary)
// replace_all handles zero-width specially
let result = re.replace_all(text, "X");
assert_eq!(result, "XaX XbX XcX");
// replace: First match only
let result = re.replace(text, "X");
assert_eq!(result, "Xa b c");
// Zero-width matches don't consume characters
// So they can appear adjacent without "overlapping"
// in the traditional sense
}Zero-width matches are a special case that both methods handle.
Empty Matches and Infinite Loops
use regex::Regex;
fn empty_matches() {
// Pattern that can match empty string
let re = Regex::new(r"a*").unwrap(); // Zero or more 'a'
let text = "b";
// replace_all handles empty matches specially
// It advances at least one position after an empty match
// This prevents infinite loops
let result = re.replace_all(text, "X");
// Empty match at 0, then at 1 (after 'b')
// replace: Same logic for first match
let result = re.replace(text, "X");
// Both methods are protected against infinite loops
}Both methods handle empty matches safely without infinite loops.
Performance Implications
use regex::Regex;
fn performance() {
let re = Regex::new(r"\d+").unwrap();
let text = "1 2 3 4 5 6 7 8 9 10";
// replace: O(n) where n = text length
// Stops after first match
// replace_all: O(n) where n = text length
// May do more replacement work
// For a text with many matches:
// - replace: Constant overhead after first match
// - replace_all: Overhead proportional to match count
// Both use the same regex engine
// Difference is in how many replacements are made
// When you only need the first replacement:
// Use replace (don't use replace_all)
// When you need all replacements:
// Use replace_all (don't loop with replace)
}replace is more efficient when you only need one replacement.
Practical Examples
use regex::Regex;
fn practical_examples() {
// Example 1: Sanitize HTML (first occurrence)
let re = Regex::new(r"<script>.*?</script>").unwrap();
let html = "<script>bad</script> some text <script>worse</script>";
let safe = re.replace(html, "");
// Only removes first script tag
// Example 2: Sanitize all HTML tags
let safe = re.replace_all(html, "");
// Removes all script tags
// Example 3: Template substitution
let re = Regex::new(r"\{\{(\w+)\}\}").unwrap();
let template = "Hello {{name}}, your score is {{score}}";
let result = re.replace_all(template, "REPLACED");
// All placeholders replaced
// Example 4: Format first URL specially
let re = Regex::new(r"https?://\S+").unwrap();
let text = "Visit https://example.com and https://other.com";
let result = re.replace(text, "[LINK]");
// Only first URL replaced
// Example 5: Increment counters
let re = Regex::new(r"\d+").unwrap();
let text = "items: 1, 2, 3";
let result = re.replace_all(text, |caps: ®ex::Captures| {
let n: i32 = caps[0].parse().unwrap();
format!("{}", n + 1)
});
// "items: 2, 3, 4"
}Choose replace or replace_all based on whether you need one or all matches replaced.
Synthesis
Comparison table:
| Aspect | replace |
replace_all |
|---|---|---|
| Matches processed | First match only | All matches |
| Overlapping matches | Not supported | Not supported |
| Return type | Cow<str> |
Cow<str> |
| Callbacks | Supported | Supported |
| Performance | Stops after first | Processes all |
| Use case | Single replacement | Multiple replacements |
Key insight: Neither replace nor replace_all can find overlapping matches. Both methods search left-to-right and continue from the end of each match, which means overlapping patterns (like finding "aba" in "ababa") will only find the first occurrence at each starting position. The only difference between the two methods is that replace stops after the first match while replace_all continues searching for additional non-overlapping matches. If you need overlapping matches, you must use find_at in a loop to manually search from each position—there's no built-in replacement method for overlapping patterns. Use replace when you only need the first match; use replace_all when you need all matches.
