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:
| Pattern | Meaning | Matching example |
|---|---|---|
. | Any character except newline | a.c matches abc, axc |
\d | Digit (0–9) | \d+ matches 123 |
\D | Not a digit | \D+ matches abc |
\w | Word char (a-z, A-Z, 0-9, _) | \w+ matches hello_42 |
\W | Not a word char | \W+ matches !@ |
\s | Whitespace (space, tab, newline) | \s+ matches |
\S | Not whitespace | \S+ matches word |
^ | Start of string (or line with (?m)) | ^Hello |
$ | End of string (or line with (?m)) | world$ |
* | 0 or more | ab*c matches ac, abc, abbc |
+ | 1 or more | ab+c matches abc, abbc |
? | 0 or 1 | ab?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|b | a or b | kucing|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: ®ex::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: ®ex::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: ®ex::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 (\\dbecomes\d). Always use raw strings for regex patterns.- Compile once, use many times —
Regex::newis relatively expensive. Uselazy_static!orOnceLockto store aRegexas a global static, rather than compiling inside a function or loop.is_matchfor validation,findfor positions,capturesfor extraction — pick the method according to what you need; don’t usecapturesif you only needis_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_allwith a closure for dynamic replacement — when the replacement text needs to be computed from the match content, use a closure as thereplace_allargument.RegexSetfor classification — more efficient than checking many patterns one by one; suitable for routing, file classification, and format detection.- The
regexcrate doesn’t support lookahead/lookbehind — this is the trade-off for the O(n) guarantee. If you need these features, consider thefancy-regexcrate (with the caveat: it can be exponential).- Always handle compilation errors —
Regex::newreturns aResult; useunwrap()only for patterns already verified correct (like inlazy_static), or handle withmatchfor patterns coming from user input.