Regex #

Regex in Rust isn’t in the standard library — you need the regex crate. But unlike regex implementations in many languages, Rust’s regex crate is designed with strong safety guarantees: it only supports regular expressions that are guaranteed to run in linear time (O(n)) relative to input length. This means no exponential backtracking — the notorious security hole in regex implementations in other languages. The trade-off: some regex features like lookahead, lookbehind, and backreferences aren’t available. This article covers everything you need: from basic syntax to named capture groups, replacement with closures, efficient compilation with lazy_static, and ready-to-use validation patterns.

Installation #

# Cargo.toml
[dependencies]
regex = "1"

# For one-time compilation across calls (recommended):
lazy_static = "1"
# or use std::sync::OnceLock (Rust 1.70+, no extra dependency)

Regex Pattern Syntax #

Before diving into code, it’s important to understand the basic patterns available. The regex crate uses syntax compatible with RE2:

PatternMeaningMatching example
.Any character except newlinea.c matches abc, axc
\dDigit (0–9)\d+ matches 123
\DNot a digit\D+ matches abc
\wWord char (a-z, A-Z, 0-9, _)\w+ matches hello_42
\WNot a word char\W+ matches !@
\sWhitespace (space, tab, newline)\s+ matches
\SNot whitespace\S+ matches word
^Start of string (or line with (?m))^Hello
$End of string (or line with (?m))world$
*0 or moreab*c matches ac, abc, abbc
+1 or moreab+c matches abc, abbc
?0 or 1ab?c matches ac, abc
{n}Exactly n times\d{4} matches 2024
{n,m}n to m times\d{2,4} matches 12, 1234
[abc]One of a, b, c[aeiou] matches vowels
[^abc]Not a, b, or c[^0-9] not a digit
(abc)Capture group(\d+) captures the number
(?P<nama>...)Named capture group(?P<tahun>\d{4})
a|ba or bkucing|anjing
(?i)Case-insensitive(?i)hello matches HELLO
(?m)Multiline (^ and $ per line)(?m)^kata
(?s)Single-line (. matches newline)(?s).+

Basic Operations #

is_match — Checking for a Match #

use regex::Regex;

fn main() {
    let pola_angka = Regex::new(r"\d+").unwrap();
    let pola_email = Regex::new(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$").unwrap();

    // is_match — just check present/absent
    println!("{}", pola_angka.is_match("there is 42 here"));  // true
    println!("{}", pola_angka.is_match("no numbers here")); // false

    // Case-insensitive
    let pola_ci = Regex::new(r"(?i)hello").unwrap();
    println!("{}", pola_ci.is_match("HELLO WORLD"));  // true
    println!("{}", pola_ci.is_match("Hello Rust"));   // true

    // Simple email validation
    let alamat_valid = "[email protected]";
    let alamat_tidak_valid = "not-an-email";
    println!("'{}' valid: {}", alamat_valid, pola_email.is_match(alamat_valid));
    println!("'{}' valid: {}", alamat_tidak_valid, pola_email.is_match(alamat_tidak_valid));
}

find — Finding the First Match #

use regex::Regex;

fn main() {
    let re = Regex::new(r"\d+").unwrap();
    let teks = "Price: 25000 rupiah, 10 percent discount";

    // find — find the first match
    match re.find(teks) {
        Some(m) => {
            println!("First found: '{}'", m.as_str());  // "25000"
            println!("Position: {}..{}", m.start(), m.end());   // 7..12
        }
        None => println!("Not found"),
    }

    // find with a specific starting position
    // (use find on a slice)
    if let Some(m) = re.find(&teks[15..]) {
        println!("After position 15: '{}'", m.as_str());  // "10"
    }
}

captures — Extracting Specific Parts #

Capture groups let you extract specific parts of matching text:

use regex::Regex;

fn main() {
    // Numbered capture groups
    let re_tanggal = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let teks = "Birth date: 1995-08-24";

    if let Some(caps) = re_tanggal.captures(teks) {
        // caps[0] = the whole match, caps[1..] = first group onwards
        println!("Whole: {}", &caps[0]);  // "1995-08-24"
        println!("Year: {}", &caps[1]);         // "1995"
        println!("Month: {}", &caps[2]);         // "08"
        println!("Day: {}", &caps[3]);           // "24"
    }

    // Named capture groups — easier to read
    let re_url = Regex::new(
        r"(?P<protokol>https?)://(?P<domain>[\w.-]+)(?P<path>/[\w./]*)?",
    ).unwrap();

    let url = "https://www.contoh.com/artikel/rust";

    if let Some(caps) = re_url.captures(url) {
        println!("Protocol: {}", &caps["protokol"]);  // "https"
        println!("Domain: {}", &caps["domain"]);       // "www.contoh.com"

        // Optional named groups — use get() rather than direct indexing
        match caps.name("path") {
            Some(p) => println!("Path: {}", p.as_str()),  // "/artikel/rust"
            None => println!("No path"),
        }
    }
}

Iterating Over All Matches #

find_iter — Iterating Over All Matches #

use regex::Regex;

fn main() {
    let re = Regex::new(r"\d+").unwrap();
    let teks = "There are 3 cats, 12 dogs, and 7 birds in the garden";

    // Collect all numbers
    let angka: Vec<&str> = re.find_iter(teks)
        .map(|m| m.as_str())
        .collect();
    println!("All numbers: {:?}", angka);  // ["3", "12", "7"]

    // Sum all numbers
    let total: u32 = re.find_iter(teks)
        .filter_map(|m| m.as_str().parse().ok())
        .sum();
    println!("Total: {}", total);  // 22

    // With positions
    for m in re.find_iter(teks) {
        println!("'{}' at position {}", m.as_str(), m.start());
    }
}

captures_iter — Iterating Over All Capture Groups #

use regex::Regex;

fn main() {
    // Extract all key=value pairs from a configuration
    let re = Regex::new(r"(?P<kunci>\w+)\s*=\s*(?P<nilai>[^\n]+)").unwrap();
    let config = "
        host = localhost
        port = 8080
        debug = true
        nama_app = Rust App
    ";

    let mut konfigurasi = std::collections::HashMap::new();
    for caps in re.captures_iter(config) {
        let kunci = caps["kunci"].trim().to_string();
        let nilai = caps["nilai"].trim().to_string();
        konfigurasi.insert(kunci, nilai);
    }

    println!("{:#?}", konfigurasi);

    // Extract all dates from text
    let re_tgl = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let log = "Event 2024-01-15: login. Event 2024-03-22: logout. Event 2024-07-10: update.";

    for caps in re_tgl.captures_iter(log) {
        println!("Date: {}/{}/{}", &caps[3], &caps[2], &caps[1]);
    }
}

Text Replacement #

replace and replace_all #

use regex::Regex;

fn main() {
    // replace — replace the first match
    let re = Regex::new(r"\d+").unwrap();
    let hasil = re.replace("there are 42 apples and 7 oranges", "N");
    println!("{}", hasil);  // "there are N apples and 7 oranges"

    // replace_all — replace every match
    let hasil_semua = re.replace_all("there are 42 apples and 7 oranges", "N");
    println!("{}", hasil_semua);  // "there are N apples and 7 oranges"

    // Replacement with capture groups ($1, $2, ...)
    let re_tgl = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
    let teks = "Born: 1995-08-24, Married: 2020-06-15";

    // Convert from YYYY-MM-DD to DD/MM/YYYY
    let hasil_tgl = re_tgl.replace_all(teks, "$3/$2/$1");
    println!("{}", hasil_tgl);  // "Born: 24/08/1995, Married: 15/06/2020"

    // Named groups in replacement
    let re_nama = Regex::new(r"(?P<depan>\w+)\s+(?P<belakang>\w+)").unwrap();
    let nama = "Budi Santoso";
    let dibalik = re_nama.replace(nama, "$belakang, $depan");
    println!("{}", dibalik);  // "Santoso, Budi"
}

replace_all with a Closure — Dynamic Replacement #

A closure enables replacement computed from the match content:

use regex::Regex;

fn main() {
    // Mask all credit card numbers (show only the last 4 digits)
    let re_cc = Regex::new(r"\b(\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?)(\d{4})\b").unwrap();
    let teks = "Card: 1234 5678 9012 3456 and 9876-5432-1098-7654";

    let masked = re_cc.replace_all(teks, |caps: &regex::Captures| {
        format!("****-****-****-{}", &caps[2])
    });
    println!("{}", masked);

    // Convert every word to title case
    let re_kata = Regex::new(r"\b\w+\b").unwrap();
    let kalimat = "halo dunia dari rust";

    let title_case = re_kata.replace_all(kalimat, |caps: &regex::Captures| {
        let kata = &caps[0];
        let mut chars = kata.chars();
        match chars.next() {
            None => String::new(),
            Some(c) => c.to_uppercase().to_string() + chars.as_str(),
        }
    });
    println!("{}", title_case);  // "Halo Dunia Dari Rust"

    // Increase all numbers by 10
    let re_angka = Regex::new(r"\d+").unwrap();
    let data = "Item A: 5, Item B: 12, Item C: 3";

    let naik = re_angka.replace_all(data, |caps: &regex::Captures| {
        let n: u32 = caps[0].parse().unwrap();
        (n + 10).to_string()
    });
    println!("{}", naik);  // "Item A: 15, Item B: 22, Item C: 13"
}

Efficient Compilation — Don’t Compile in a Loop #

Compiling a Regex is a relatively expensive operation. Recompiling the same pattern repeatedly inside a loop or a frequently called function is an often-overlooked anti-pattern:

use regex::Regex;

// ANTI-PATTERN: compiling every time the function is called
fn validasi_email_buruk(email: &str) -> bool {
    let re = Regex::new(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$").unwrap();
    re.is_match(email)
}

// CORRECT with lazy_static — compile once, use forever
use lazy_static::lazy_static;

lazy_static! {
    static ref RE_EMAIL: Regex = Regex::new(
        r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
    ).unwrap();

    static ref RE_TELEPON: Regex = Regex::new(
        r"^(\+62|62|0)8[1-9]\d{6,10}$"
    ).unwrap();
}

fn validasi_email(email: &str) -> bool {
    RE_EMAIL.is_match(email)
}

fn validasi_telepon(nomor: &str) -> bool {
    RE_TELEPON.is_match(nomor)
}

// Alternative with std::sync::OnceLock (Rust 1.70+, no lazy_static)
use std::sync::OnceLock;

fn re_tanggal() -> &'static Regex {
    static RE: OnceLock<Regex> = OnceLock::new();
    RE.get_or_init(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap())
}

fn main() {
    // Validate many emails — RE_EMAIL is only compiled once
    let emails = [
        "[email protected]",
        "tidak-valid",
        "[email protected]",
        "joko@",
    ];

    for email in &emails {
        println!("{}: {}", email, validasi_email(email));
    }

    // Validate Indonesian phone numbers
    let nomor = ["081234567890", "62812345678", "+628****7890", "12345"];
    for n in &nomor {
        println!("{}: {}", n, validasi_telepon(n));
    }

    // Date validation with OnceLock
    println!("2024-08-24 valid: {}", re_tanggal().is_match("2024-08-24"));
    println!("24-08-2024 valid: {}", re_tanggal().is_match("24-08-2024"));
}

RegexSet — Matching Many Patterns at Once #

RegexSet matches a single string against many patterns in one operation — far more efficient than checking them one by one:

use regex::RegexSet;

fn main() {
    // Classify content types by URL/extension patterns
    let set = RegexSet::new(&[
        r"\.jpg$|\.jpeg$|\.png$|\.gif$|\.webp$",  // 0: image
        r"\.mp4$|\.avi$|\.mov$|\.mkv$",            // 1: video
        r"\.mp3$|\.wav$|\.ogg$|\.flac$",           // 2: audio
        r"\.pdf$|\.doc$|\.docx$|\.xlsx$",          // 3: document
        r"\.rs$|\.py$|\.js$|\.go$",                // 4: code
    ]).unwrap();

    let nama_label = ["Image", "Video", "Audio", "Document", "Code"];

    let file_list = [
        "foto.jpg",
        "video.mp4",
        "lagu.mp3",
        "laporan.pdf",
        "main.rs",
        "tidak-dikenal.xyz",
    ];

    for file in &file_list {
        let cocok: Vec<&str> = set.matches(file)
            .iter()
            .map(|i| nama_label[i])
            .collect();

        if cocok.is_empty() {
            println!("{}: unknown", file);
        } else {
            println!("{}: {}", file, cocok.join(", "));
        }
    }
}

RegexBuilder — Advanced Configuration #

RegexBuilder lets you configure various options before compiling a regex:

use regex::RegexBuilder;

fn main() {
    // Case-insensitive
    let re_ci = RegexBuilder::new(r"hello world")
        .case_insensitive(true)
        .build()
        .unwrap();

    println!("{}", re_ci.is_match("HELLO WORLD"));  // true
    println!("{}", re_ci.is_match("Hello World"));  // true

    // Multiline — ^ and $ apply per line
    let re_ml = RegexBuilder::new(r"^\w+")
        .multi_line(true)
        .build()
        .unwrap();

    let teks_ml = "baris pertama\nbaris kedua\nbaris ketiga";
    for m in re_ml.find_iter(teks_ml) {
        println!("Match: '{}'", m.as_str());
        // "baris", "baris", "baris" — start of every line
    }

    // Dot matches newlines
    let re_dot = RegexBuilder::new(r"Mulai.*Selesai")
        .dot_matches_new_line(true)
        .build()
        .unwrap();

    let teks_ml2 = "Mulai\nbaris tengah\nSelesai";
    println!("Multiline match: {}", re_dot.is_match(teks_ml2));  // true

    // Limit the size to protect against ReDoS
    let re_terbatas = RegexBuilder::new(r"\w+")
        .size_limit(1024 * 1024)  // max 1MB for regex bytecode
        .build()
        .unwrap();
}

Ready-to-Use Validation Patterns #

A collection of common validation patterns that can be used directly:

use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
    // Email (simple — for strict validation use a dedicated library)
    static ref RE_EMAIL: Regex = Regex::new(
        r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$"
    ).unwrap();

    // Indonesian phone number
    static ref RE_TELEPON_ID: Regex = Regex::new(
        r"^(\+62|62|0)(21|22|24|31|274|361|411|751|778|811|812|813|821|822|823|851|852|853|855|856|857|858|859|877|878|896|897|898|899)\d{5,10}$"
    ).unwrap();

    // Indonesian postal code (5 digits)
    static ref RE_KODEPOS: Regex = Regex::new(
        r"^\d{5}$"
    ).unwrap();

    // NIK (16 digits)
    static ref RE_NIK: Regex = Regex::new(
        r"^\d{16}$"
    ).unwrap();

    // URL with protocol
    static ref RE_URL: Regex = Regex::new(
        r"^https?://[\w\-]+(\.[\w\-]+)+([\w\-\._~:/?#\[\]@!\$&'\(\)\*\+,;=%]+)?$"
    ).unwrap();

    // YYYY-MM-DD date format
    static ref RE_TANGGAL_ISO: Regex = Regex::new(
        r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$"
    ).unwrap();

    // Letters and spaces only (person names)
    static ref RE_NAMA: Regex = Regex::new(
        r"^[a-zA-Z\s'-]{2,100}$"
    ).unwrap();
}

fn main() {
    let data_uji = [
        ("Valid email",     RE_EMAIL.is_match("[email protected]")),
        ("Invalid email", RE_EMAIL.is_match("bukan@email")),
        ("Valid postal code",  RE_KODEPOS.is_match("40115")),
        ("Invalid postal code",  RE_KODEPOS.is_match("4011")),
        ("Valid URL",       RE_URL.is_match("https://www.rust-lang.org")),
        ("Valid date",   RE_TANGGAL_ISO.is_match("2024-08-24")),
        ("Invalid date",   RE_TANGGAL_ISO.is_match("2024-13-45")),
    ];

    for (deskripsi, hasil) in &data_uji {
        println!("{}: {}", deskripsi, hasil);
    }
}

Summary #

  • Raw string r"..." for regex patterns — avoids double-escaping (\\d becomes \d). Always use raw strings for regex patterns.
  • Compile once, use many timesRegex::new is relatively expensive. Use lazy_static! or OnceLock to store a Regex as a global static, rather than compiling inside a function or loop.
  • is_match for validation, find for positions, captures for extraction — pick the method according to what you need; don’t use captures if you only need is_match.
  • Named capture groups (?P<nama>...) are easier to read — access with &caps["nama"] rather than &caps[1], which is error-prone when the pattern changes.
  • replace_all with a closure for dynamic replacement — when the replacement text needs to be computed from the match content, use a closure as the replace_all argument.
  • RegexSet for classification — more efficient than checking many patterns one by one; suitable for routing, file classification, and format detection.
  • The regex crate doesn’t support lookahead/lookbehind — this is the trade-off for the O(n) guarantee. If you need these features, consider the fancy-regex crate (with the caveat: it can be exponential).
  • Always handle compilation errorsRegex::new returns a Result; use unwrap() only for patterns already verified correct (like in lazy_static), or handle with match for patterns coming from user input.

← Previous: Date & Time   Next: Crates →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact