regex Crate (Rust): Capture Groups & Performance Guide

Max WellsMax WellsFounder of Rustify

TL;DR: The regex crate provides Rust's standard regular expression support. It is fast (linear time guarantee, no catastrophic backtracking), safe (no unsafe code in the regex engine), and Unicode-aware by default. Compile patterns with Regex::new(), check matches with .is_match(), extract captures with .captures(), and replace text with .replace(). Compile regexes once with LazyLock or once_cell; recompiling on every call is expensive.


How Do You Use the regex Crate?

# Cargo.toml
[dependencies]
regex = "1"
use regex::Regex;
 
fn main() {
    let re = Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap();  // yyyy-mm-dd
 
    // Check if pattern exists
    println!("{}", re.is_match("Today is 2026-04-15"));  // true
    println!("{}", re.is_match("No date here"));          // false
 
    // Find the match
    if let Some(mat) = re.find("Event on 2026-04-15 at noon") {
        println!("found: {} ({}..{})", mat.as_str(), mat.start(), mat.end());
        // found: 2026-04-15 (9..19)
    }
}

How Do You Extract Capture Groups?

Wrap parts of the pattern in (...) to create capture groups; access them by index or name.

use regex::Regex;
 
fn main() {
    // Named captures: (?P<name>pattern)
    let re = Regex::new(
        r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
    ).unwrap();
 
    let text = "Launch date: 2026-04-15";
 
    if let Some(caps) = re.captures(text) {
        println!("year:  {}", &caps["year"]);   // 2026
        println!("month: {}", &caps["month"]);  // 04
        println!("day:   {}", &caps["day"]);    // 15
 
        // By index
        println!("full match: {}", caps.get(0).unwrap().as_str());  // 2026-04-15
    }
}

How Do You Find All Matches?

.find_iter() returns an iterator over all non-overlapping matches.

use regex::Regex;
 
fn main() {
    let re = Regex::new(r"\b\w+@\w+\.\w+\b").unwrap();  // simple email pattern
    let text = "Contact [email protected] or [email protected] for info";
 
    for mat in re.find_iter(text) {
        println!("email: {}", mat.as_str());
    }
    // email: [email protected]
    // email: [email protected]
 
    // Collect all matches
    let emails: Vec<&str> = re.find_iter(text).map(|m| m.as_str()).collect();
}

How Do You Replace Text?

.replace() replaces the first match; .replace_all() replaces all matches. Use $1, $name in the replacement string.

use regex::Regex;
 
fn main() {
    let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let text = "Date: 2026-04-15, Updated: 2026-03-01";
 
    // Replace first match; reorder to US format (mm/dd/yyyy)
    let result = re.replace(text, "$2/$3/$1");
    println!("{result}");  // Date: 04/15/2026, Updated: 2026-03-01
 
    // Replace all matches
    let result = re.replace_all(text, "$2/$3/$1");
    println!("{result}");  // Date: 04/15/2026, Updated: 03/01/2026
 
    // Dynamic replacement with closure
    let result = re.replace_all(text, |caps: &regex::Captures| {
        format!("{}/{}/{}", &caps[2], &caps[3], &caps[1])
    });
}

How Do You Avoid Recompiling Regexes on Every Call?

Compile once with LazyLock (Rust 1.80+) or once_cell::sync::Lazy; recompiling on each call is the most common performance mistake.

use std::sync::LazyLock;
use regex::Regex;
 
// Compiled once at first use; zero-cost thereafter
static EMAIL_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap()
});
 
static DATE_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap()
});
 
fn validate_email(input: &str) -> bool {
    EMAIL_RE.is_match(input)  // no compilation overhead
}

Frequently Asked Questions

Yes; the regex crate uses a finite automaton engine that guarantees O(n) time complexity (linear in input length). It does not support backreferences or lookahead/lookbehind, which are the features that cause exponential backtracking in other regex engines. For those features, use the fancy-regex crate (but with no backtracking guarantees).

Use the (?i) flag at the start of the pattern:

let re = Regex::new(r"(?i)hello").unwrap();
re.is_match("HELLO");  // true

The regex! macro (from the regex crate) compiles the regex at compile time; no unwrap() needed, no runtime cost. Available in nightly or via the once_cell pattern. For most projects, LazyLock is simpler.

The regex crate requires std. For no_std use, try regex-automata (the underlying engine, available no_std) or regex-lite (a smaller, simpler subset).


Sources


  • String: regex operates on string slices
  • Iterator: .find_iter() and .captures_iter() return iterators
  • once-cell: Use for compiling regexes once

Keep Reading

Ready to Land a $80-120k Rust Job?