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: &regex::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.