Loading page…
Rust walkthroughs
Loading page…
regex::Regex::replace_all handle captured groups in replacement strings?regex::Regex::replace_all substitutes all matches of a pattern with a replacement string, interpreting special syntax like $1, $2, and $name to reference captured groups from the match. The replacement string is processed to expand capture group references into their matched content, enabling dynamic replacements based on what was captured. Named capture groups use $name syntax, numbered captures use $1 through $99, and the entire match is referenced with $0. For complex replacement logic that can't be expressed in a string, replace_all accepts a closure that receives Captures and returns a Cow<str>. The replacement string syntax treats $ specially—use $$ to produce a literal $ character in the output.
use regex::Regex;
fn main() {
let text = "The quick brown fox jumps over the lazy dog.";
// Simple replacement with literal string
let re = Regex::new("fox").unwrap();
let result = re.replace_all(text, "cat");
println!("Result: {}", result);
// "The quick brown cat jumps over the lazy dog."
// Replace all occurrences
let re = Regex::new("the").unwrap();
let result = re.replace_all(text, "a");
println!("Result: {}", result);
// "a quick brown fox jumps over a lazy dog."
// Note: 'the' is case-sensitive, matches "the" but not "The"
}When the replacement contains no $ references, it's treated as a literal string.
use regex::Regex;
fn main() {
// Capture groups are referenced by $1, $2, etc.
let text = "John Smith";
let re = Regex::new(r"(\w+) (\w+)").unwrap();
// $1 = first capture group, $2 = second capture group
let result = re.replace_all(text, "$2, $1");
println!("Swapped: {}", result);
// "Smith, John"
// Multiple matches - replace_all processes all
let text = "apple banana cherry";
let re = Regex::new(r"(\w)(\w+)").unwrap();
// $1 = first letter, $2 = rest of word
let result = re.replace_all(text, "$1-$2");
println!("With dash: {}", result);
// "a-pple b-anana c-herry"
// $0 is the entire match
let text = "hello world";
let re = Regex::new(r"\w+").unwrap();
let result = re.replace_all(text, "[$0]");
println!("Bracketed: {}", result);
// "[hello] [world]"
}$1 through $99 reference numbered capture groups; $0 references the entire match.
use regex::Regex;
fn main() {
// Named captures use $name syntax
let text = "2024-03-15";
let re = Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})").unwrap();
// Reference named groups by name
let result = re.replace_all(text, "$month/$day/$year");
println!("Reformatted: {}", result);
// "03/15/2024"
// Named groups can also be referenced by number
let result = re.replace_all(text, "$2/$3/$1");
println!("By number: {}", result);
// "03/15/2024"
// Mix named and numbered references
let text = "user@example.com";
let re = Regex::new(r"(?P<user>\w+)@(?P<domain>[\w.]+)").unwrap();
let result = re.replace_all(text, "$user at $domain");
println!("Email: {}", result);
// "user at example.com"
}Named captures can be referenced by $name or by their position number.
use regex::Regex;
fn main() {
// $$ produces a literal $ in the replacement
let text = "Price: 100";
let re = Regex::new(r"(\d+)").unwrap();
let result = re.replace_all(text, "$$$1");
println!("With dollar sign: {}", result);
// "Price: $100"
// Without $$, $ would be interpreted as a capture reference
let text = "item1 item2";
let re = Regex::new(r"item(\d)").unwrap();
// $$1 means literal $ followed by group 1's content
let result = re.replace_all(text, "$$$1");
println!("Escaped: {}", result);
// "$1 $2"
// $1 alone would reference the capture
let result = re.replace_all(text, "$1");
println!("Unescaped: {}", result);
// "1 2"
}Use $$ to include a literal $ in the replacement string.
use regex::Regex;
fn main() {
// Ambiguity: $10 could mean group 10 or group 1 followed by "0"
let text = "abcdefghij";
let re = Regex::new(r"(.)").unwrap();
// Regex resolves ambiguity by preferring larger numbers
// If group 10 doesn't exist, it uses group 1 + literal "0"
// Explicit group reference with braces
let text = "abc";
let re = Regex::new(r"(.)(.)(.)").unwrap();
// ${1}0 means group 1 followed by literal 0
let result = re.replace_all(text, "${1}0");
println!("Explicit: {}", result);
// "a0b0c0"
// $10 would try group 10 (which doesn't exist)
// Falls back to group 1 + literal "0"
let result = re.replace_all(text, "$10");
println!("Ambiguous: {}", result);
// "a0b0c0" - same result in this case
// Named groups avoid ambiguity
let re = Regex::new(r"(?P<first>.)(?P<second>.)(?P<third>.)").unwrap();
let result = re.replace_all(text, "$first$second$third");
println!("Named: {}", result);
// "abc"
}Use ${n} to disambiguate when a number following a capture reference could be misinterpreted.
use regex::Regex;
fn main() {
// For complex logic, use a closure instead of a string
let text = "The temperatures are 32F and 100C";
let re = Regex::new(r"(\d+)F|(\d+)C").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
if let Some(fahrenheit) = caps.get(1) {
let f: i32 = fahrenheit.as_str().parse().unwrap();
let c = (f - 32) * 5 / 9;
format!("{}C", c)
} else if let Some(celsius) = caps.get(2) {
let c: i32 = celsius.as_str().parse().unwrap();
let f = c * 9 / 5 + 32;
format!("{}F", f)
} else {
caps[0].to_string()
}
});
println!("Converted: {}", result);
// "The temperatures are 0C and 212F"
// Closure receives Captures, returns String or Cow<str>
let text = "multiply 5 by 3";
let re = Regex::new(r"multiply (\d+) by (\d+)").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
let a: i32 = caps[1].parse().unwrap();
let b: i32 = caps[2].parse().unwrap();
format!("result {}", a * b)
});
println!("Computed: {}", result);
// "result 15"
}Closures enable complex replacement logic that can't be expressed with string interpolation.
use regex::Regex;
fn main() {
let text = "Name: John Doe, Age: 30";
let re = Regex::new(r"Name: (\w+) (\w+), Age: (\d+)").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
// Access captures by index
let first = &caps[1];
let last = &caps[2];
let age: u32 = caps[3].parse().unwrap();
// Process and format
let full_name = format!("{} {} ({})", first, last, age);
full_name
});
println!("Formatted: {}", result);
// "John Doe (30)"
// Named captures in closures
let text = "2024-03-15";
let re = Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
// Access by name
let year = caps.name("year").unwrap().as_str();
let month = caps.name("month").unwrap().as_str();
let day = caps.name("day").unwrap().as_str();
// Can also access by index: caps[1], caps[2], caps[3]
format!("{}/{}/{}", month, day, year)
});
println!("Date: {}", result);
// "03/15/2024"
// Check if a capture matched
let text = "color: red";
let re = Regex::new(r"color: (\w+)(?:, shade: (\w+))?").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
let color = &caps[1];
if let Some(shade) = caps.get(2) {
format!("{} ({})", color, shade.as_str())
} else {
color.to_string()
}
});
println!("Color: {}", result);
}Use caps[n] for indexed access, caps.name("n") for named access, and caps.get(n) for optional captures.
use regex::Regex;
fn main() {
// replace() only replaces the first match
let text = "aaa";
let re = Regex::new("a").unwrap();
let result = re.replace(text, "b");
println!("replace: {}", result);
// "baa"
// replace_all() replaces all matches
let result = re.replace_all(text, "b");
println!("replace_all: {}", result);
// "bbb"
// With capture groups
let text = "word1 word2 word3";
let re = Regex::new(r"word(\d+)").unwrap();
let result = re.replace(text, "WORD$1");
println!("replace: {}", result);
// "WORD1 word2 word3"
let result = re.replace_all(text, "WORD$1");
println!("replace_all: {}", result);
// "WORD1 WORD2 WORD3"
}replace modifies only the first match; replace_all modifies all matches.
use regex::Regex;
fn main() {
// Optional captures that didn't match are empty
let text = "hello";
let re = Regex::new(r"(\w+)(?: (\w+))?").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
let first = &caps[1];
let second = caps.get(2).map(|m| m.as_str()).unwrap_or("N/A");
format!("first: {}, second: {}", first, second)
});
println!("Result: {}", result);
// "first: hello, second: N/A"
// In string replacement, non-matching groups produce empty strings
let text = "hello";
let re = Regex::new(r"(\w+)( (\w+))?").unwrap();
let result = re.replace_all(text, "[$1][$2][$3]");
println!("Bracketed: {}", result);
// "[hello][]" - group 2 and 3 are empty
// The whole pattern matches, but optional groups may not
let text = "value: 42";
let re = Regex::new(r"value: (?:(\d+)|(\w+))").unwrap();
let result = re.replace_all(text, |caps: ®ex::Captures| {
if let Some(num) = caps.get(1) {
format!("number: {}", num.as_str())
} else if let Some(word) = caps.get(2) {
format!("word: {}", word.as_str())
} else {
"unknown".to_string()
}
});
println!("Typed: {}", result);
// "number: 42"
}Optional captures that didn't participate in the match return None from get().
use regex::Regex;
fn main() {
// Each match processes its own capture groups
let text = "name: John, age: 30; name: Jane, age: 25";
let re = Regex::new(r"name: (\w+), age: (\d+)").unwrap();
let result = re.replace_all(text, "$1 (age $2)");
println!("Formatted: {}", result);
// "John (age 30); Jane (age 25)"
// Captures are per-match, not global
let text = "a=1 b=2 c=3";
let re = Regex::new(r"(\w)=(\d)").unwrap();
let result = re.replace_all(text, "[$1=$2]");
println!("Bracketed: {}", result);
// "[a=1] [b=2] [c=3]"
// Each occurrence gets its own capture groups
}Each match has its own set of capture group values; replacements are independent.
use regex::Regex;
fn main() {
// Markdown links to HTML
let text = "Check [example](https://example.com) and [docs](https://docs.rs)";
let re = Regex::new(r"\[(?P<text>[^]]+)\]\((?P<url>[^)]+)\)").unwrap();
let result = re.replace_all(text, r#"<a href="$url">$text</a>"#);
println!("HTML: {}", result);
// <a href="https://example.com">example</a> and <a href="https://docs.rs">docs</a>
// Template variable substitution
let text = "Hello {{name}}, your balance is {{amount}}";
let re = Regex::new(r"\{\{(\w+)\}\}").unwrap();
// In real code, look up variables in a map
let result = re.replace_all(text, |caps: ®ex::Captures| {
let var = &caps[1];
match var {
"name" => "Alice".to_string(),
"amount" => "$100.00".to_string(),
_ => format!("{{{{{}}}}}", var), // Keep unknown variables
}
});
println!("Substituted: {}", result);
// "Hello Alice, your balance is $100.00"
// Code transformation
let text = "function foo() { return 1; }";
let re = Regex::new(r"function (\w+)\(\) \{ return (\d+); \}").unwrap();
let result = re.replace_all(text, "fn $1() -> i32 { $2 }");
println!("Transformed: {}", result);
// "fn foo() -> i32 { 1 }"
}Real-world replacements often combine named captures with structured output.
use regex::Regex;
fn main() {
// Only $ is special in replacement strings
// Backslash escapes are NOT processed
let text = "hello\\nworld";
let re = Regex::new(r"\\n").unwrap();
// This replaces literal \n with actual newline
let result = re.replace_all(text, "\n");
println!("With newline:\n{}", result);
// But in the replacement string, \n is literal backslash + n
// Use actual escape sequences in Rust strings
// To get a literal backslash in output
let text = "path/to/file";
let re = Regex::new(r"/").unwrap();
let result = re.replace_all(text, "\\");
println!("Backslashes: {}", result);
// "path\to\file"
// Common confusion: $$ vs $
let text = "price: 100";
let re = Regex::new(r"(\d+)").unwrap();
// Wrong: $ is interpreted as capture reference
// "$100" would look for group 100
// Correct: $$ for literal $
let result = re.replace_all(text, "$$$1");
println!("Money: {}", result);
// "price: $100"
}Only $ is special in replacement strings; backslash sequences must be actual Rust escape sequences.
use regex::Regex;
use std::time::Instant;
fn main() {
let text = "The quick brown fox jumps over the lazy dog. ".repeat(1000);
// String replacement is faster than closure for simple cases
let re = Regex::new(r"(\w+)").unwrap();
let start = Instant::now();
let _ = re.replace_all(&text, "[$1]");
println!("String replacement: {:?}", start.elapsed());
// Closure has more overhead but enables complex logic
let start = Instant::now();
let _ = re.replace_all(&text, |caps: ®ex::Captures| {
format!("[{}]", &caps[1])
});
println!("Closure replacement: {:?}", start.elapsed());
// For repeated replacements, pre-compile once
let re = Regex::new(r"(\w+)").unwrap();
// NoMatch substitution - fastest when replacing with captures
let result = re.replace_all(&text, "$1");
// Identity replacement, no actual change
}String replacement is faster than closures; use closures only when necessary.
use regex::Regex;
fn main() {
let text = "hello world";
let re = Regex::new(r"(\w+) (\w+)").unwrap();
// String replacement with capture references
let result = re.replace_all(text, "$2 $1");
// Named captures
let re = Regex::new(r"(?P<first>\w+) (?P<second>\w+)").unwrap();
let result = re.replace_all(text, "$second $first");
// Closure replacement
let result = re.replace_all(text, |caps: ®ex::Captures| {
format!("{} {}", &caps[2], &caps[1])
});
// Literal $ with $$
let text = "price: 100";
let re = Regex::new(r"(\d+)").unwrap();
let result = re.replace_all(text, "$$$1");
// $0 for entire match
let re = Regex::new(r"\w+").unwrap();
let result = re.replace_all(text, "[$0]");
// Disambiguate with ${n}
let text = "abc";
let re = Regex::new(r"(.)(.)(.)").unwrap();
let result = re.replace_all(text, "${1}0");
}Replacement string syntax:
| Syntax | Meaning |
|--------|---------|
| $0 | Entire match |
| $1 through $99 | Numbered capture groups |
| $name | Named capture group |
| ${name} | Named capture (disambiguated) |
| ${1} | Numbered capture (disambiguated) |
| $$ | Literal $ |
Capture group access in closures:
| Method | Returns | Use case |
|--------|---------|----------|
| caps[n] | &str | Direct access, panics if missing |
| caps.get(n) | Option<Match> | Optional captures |
| caps.name("n") | Option<Match> | Named captures |
Key differences from other engines:
| Feature | Rust regex | Others |
|---------|--------------|--------|
| Capture syntax | $1, $name | Some use \1 |
| Escape | $$ for literal $ | Varies |
| Backslash escapes | Not processed | Some process \n, etc. |
When to use each approach:
| Approach | Use when | |----------|----------| | String replacement | Simple substitution with captures | | Closure replacement | Computation needed, conditional logic | | Named captures | Readability matters, many groups | | Numbered captures | Quick one-offs, standard groups |
Key insight: replace_all processes the replacement string specially, interpreting $ as a capture group reference prefix. This is distinct from regex pattern syntax—within patterns, groups use (...) and backreferences use \1, but in replacement strings, captures use $1. The $$ escape produces a literal $ when the replacement needs a dollar sign. For transformations requiring computation (formatting, lookup, conditional logic), the closure variant receives Captures and returns Cow<str>, enabling arbitrary Rust code while still matching the pattern. Named captures make complex patterns readable in replacement strings ($year/$month/$day vs $1/$2/$3), and the get() method handles optional captures gracefully. The replace_all method applies to every non-overlapping match, while replace applies only to the first—both share the same replacement string syntax.