Regex #
Regex di Rust tidak ada di standard library — kamu membutuhkan crate regex. Tapi berbeda dari implementasi regex di banyak bahasa, crate regex Rust didesain dengan jaminan keamanan yang kuat: ia hanya mendukung regular expressions yang dijamin berjalan dalam waktu linear (O(n)) terhadap panjang input. Ini berarti tidak ada backtracking eksponensial — celah keamanan yang terkenal di implementasi regex di bahasa lain. Tradeoff-nya: beberapa fitur regex seperti lookahead, lookbehind, dan backreference tidak tersedia. Artikel ini membahas semua yang kamu butuhkan: dari sintaks dasar hingga named capture groups, penggantian dengan closure, kompilasi efisien dengan lazy_static, dan pola validasi yang siap pakai.
Instalasi #
# Cargo.toml
[dependencies]
regex = "1"
# Untuk kompilasi sekali lintas pemanggilan (direkomendasikan):
lazy_static = "1"
# atau gunakan std::sync::OnceLock (Rust 1.70+, tanpa dependensi tambahan)
Sintaks Pola Regex #
Sebelum masuk ke kode, penting memahami pola dasar yang tersedia. Crate regex menggunakan sintaks yang kompatibel dengan RE2:
| Pola | Makna | Contoh cocok |
|---|---|---|
. | Karakter apapun kecuali newline | a.c cocok abc, axc |
\d | Digit (0–9) | \d+ cocok 123 |
\D | Bukan digit | \D+ cocok abc |
\w | Word char (a-z, A-Z, 0-9, _) | \w+ cocok hello_42 |
\W | Bukan word char | \W+ cocok !@ |
\s | Whitespace (spasi, tab, newline) | \s+ cocok |
\S | Bukan whitespace | \S+ cocok kata |
^ | Awal string (atau baris dengan (?m)) | ^Hello |
$ | Akhir string (atau baris dengan (?m)) | world$ |
* | 0 atau lebih | ab*c cocok ac, abc, abbc |
+ | 1 atau lebih | ab+c cocok abc, abbc |
? | 0 atau 1 | ab?c cocok ac, abc |
{n} | Tepat n kali | \d{4} cocok 2024 |
{n,m} | n hingga m kali | \d{2,4} cocok 12, 1234 |
[abc] | Salah satu dari a, b, c | [aeiou] cocok vokal |
[^abc] | Bukan a, b, atau c | [^0-9] bukan digit |
(abc) | Capture group | (\d+) tangkap angka |
(?P<nama>...) | Named capture group | (?P<tahun>\d{4}) |
a|b | a atau b | kucing|anjing |
(?i) | Case-insensitive | (?i)hello cocok HELLO |
(?m) | Multiline (^ dan $ per baris) | (?m)^kata |
(?s) | Single-line (. cocok newline) | (?s).+ |
Operasi Dasar #
is_match — Cek Kecocokan
#
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 — hanya cek ada/tidak
println!("{}", pola_angka.is_match("ada 42 di sini")); // true
println!("{}", pola_angka.is_match("tidak ada angka")); // 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
// Validasi email sederhana
let alamat_valid = "[email protected]";
let alamat_tidak_valid = "bukan-email";
println!("'{}' valid: {}", alamat_valid, pola_email.is_match(alamat_valid));
println!("'{}' valid: {}", alamat_tidak_valid, pola_email.is_match(alamat_tidak_valid));
}
find — Temukan Kecocokan Pertama
#
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let teks = "Harga: 25000 rupiah, diskon 10 persen";
// find — temukan kecocokan pertama
match re.find(teks) {
Some(m) => {
println!("Pertama ditemukan: '{}'", m.as_str()); // "25000"
println!("Posisi: {}..{}", m.start(), m.end()); // 7..12
}
None => println!("Tidak ditemukan"),
}
// find dengan posisi awal tertentu
// (gunakan find pada slice)
if let Some(m) = re.find(&teks[15..]) {
println!("Setelah posisi 15: '{}'", m.as_str()); // "10"
}
}
captures — Ekstrak Bagian Spesifik
#
Capture groups memungkinkan kamu mengekstrak bagian tertentu dari teks yang cocok:
use regex::Regex;
fn main() {
// Numbered capture groups
let re_tanggal = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
let teks = "Tanggal lahir: 1995-08-24";
if let Some(caps) = re_tanggal.captures(teks) {
// caps[0] = keseluruhan match, caps[1..] = group pertama dst.
println!("Keseluruhan: {}", &caps[0]); // "1995-08-24"
println!("Tahun: {}", &caps[1]); // "1995"
println!("Bulan: {}", &caps[2]); // "08"
println!("Hari: {}", &caps[3]); // "24"
}
// Named capture groups — lebih mudah dibaca
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!("Protokol: {}", &caps["protokol"]); // "https"
println!("Domain: {}", &caps["domain"]); // "www.contoh.com"
// Named group yang opsional — gunakan get() bukan indeks langsung
match caps.name("path") {
Some(p) => println!("Path: {}", p.as_str()), // "/artikel/rust"
None => println!("Tidak ada path"),
}
}
}
Iterasi Semua Kecocokan #
find_iter — Iterasi Semua Match
#
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let teks = "Ada 3 kucing, 12 anjing, dan 7 burung di kebun itu";
// Kumpulkan semua angka
let angka: Vec<&str> = re.find_iter(teks)
.map(|m| m.as_str())
.collect();
println!("Semua angka: {:?}", angka); // ["3", "12", "7"]
// Jumlahkan semua angka
let total: u32 = re.find_iter(teks)
.filter_map(|m| m.as_str().parse().ok())
.sum();
println!("Total: {}", total); // 22
// Dengan posisi
for m in re.find_iter(teks) {
println!("'{}' di posisi {}", m.as_str(), m.start());
}
}
captures_iter — Iterasi Semua Capture Groups
#
use regex::Regex;
fn main() {
// Ekstrak semua pasangan kunci=nilai dari konfigurasi
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);
// Ekstrak semua tanggal dari teks
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!("Tanggal: {}/{}/{}", &caps[3], &caps[2], &caps[1]);
}
}
Penggantian Teks #
replace dan replace_all
#
use regex::Regex;
fn main() {
// replace — ganti kecocokan pertama
let re = Regex::new(r"\d+").unwrap();
let hasil = re.replace("ada 42 apel dan 7 jeruk", "N");
println!("{}", hasil); // "ada N apel dan 7 jeruk"
// replace_all — ganti semua kecocokan
let hasil_semua = re.replace_all("ada 42 apel dan 7 jeruk", "N");
println!("{}", hasil_semua); // "ada N apel dan 7 jeruk"
// Penggantian dengan capture group ($1, $2, ...)
let re_tgl = Regex::new(r"(\d{4})-(\d{2})-(\d{2})").unwrap();
let teks = "Lahir: 1995-08-24, Menikah: 2020-06-15";
// Ubah dari YYYY-MM-DD ke DD/MM/YYYY
let hasil_tgl = re_tgl.replace_all(teks, "$3/$2/$1");
println!("{}", hasil_tgl); // "Lahir: 24/08/1995, Menikah: 15/06/2020"
// Named groups dalam penggantian
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 dengan Closure — Penggantian Dinamis
#
Closure memungkinkan penggantian yang dihitung berdasarkan konten match:
use regex::Regex;
fn main() {
// Mask semua nomor kartu kredit (tunjukkan hanya 4 digit terakhir)
let re_cc = Regex::new(r"\b(\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?)(\d{4})\b").unwrap();
let teks = "Kartu: 1234 5678 9012 3456 dan 9876-5432-1098-7654";
let masked = re_cc.replace_all(teks, |caps: ®ex::Captures| {
format!("****-****-****-{}", &caps[2])
});
println!("{}", masked);
// Ubah setiap kata menjadi 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"
// Naikkan semua angka sebesar 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"
}
Kompilasi Efisien — Jangan Kompilasi dalam Loop #
Kompilasi Regex adalah operasi yang relatif mahal. Mengkompilasi pola yang sama berulang kali di dalam loop atau fungsi yang dipanggil berkali-kali adalah anti-pattern yang sering terlewat:
use regex::Regex;
// ANTI-PATTERN: kompilasi setiap kali fungsi dipanggil
fn validasi_email_buruk(email: &str) -> bool {
let re = Regex::new(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$").unwrap();
re.is_match(email)
}
// BENAR dengan lazy_static — kompilasi sekali, gunakan selamanya
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)
}
// Alternatif dengan std::sync::OnceLock (Rust 1.70+, tanpa 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() {
// Validasi banyak email — RE_EMAIL hanya dikompilasi sekali
let emails = [
"[email protected]",
"tidak-valid",
"[email protected]",
"joko@",
];
for email in &emails {
println!("{}: {}", email, validasi_email(email));
}
// Validasi nomor telepon Indonesia
let nomor = ["081234567890", "62812345678", "+6281234567890", "12345"];
for n in &nomor {
println!("{}: {}", n, validasi_telepon(n));
}
// Validasi tanggal dengan 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 — Cocokkan Banyak Pola Sekaligus
#
RegexSet mencocokkan satu string terhadap banyak pola dalam satu operasi — jauh lebih efisien dari mengecek satu per satu:
use regex::RegexSet;
fn main() {
// Klasifikasi tipe konten berdasarkan pola URL/ekstensi
let set = RegexSet::new(&[
r"\.jpg$|\.jpeg$|\.png$|\.gif$|\.webp$", // 0: gambar
r"\.mp4$|\.avi$|\.mov$|\.mkv$", // 1: video
r"\.mp3$|\.wav$|\.ogg$|\.flac$", // 2: audio
r"\.pdf$|\.doc$|\.docx$|\.xlsx$", // 3: dokumen
r"\.rs$|\.py$|\.js$|\.go$", // 4: kode
]).unwrap();
let nama_label = ["Gambar", "Video", "Audio", "Dokumen", "Kode"];
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!("{}: tidak dikenal", file);
} else {
println!("{}: {}", file, cocok.join(", "));
}
}
}
RegexBuilder — Konfigurasi Lanjutan
#
RegexBuilder memungkinkan mengkonfigurasi berbagai opsi sebelum mengkompilasi 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 — ^ dan $ berlaku per baris
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" — awal setiap baris
}
// Dot cocok newline
let re_dot = RegexBuilder::new(r"Mulai.*Selesai")
.dot_matches_new_line(true)
.build()
.unwrap();
let teks_ml2 = "Mulai\nbaris tengah\nSelesai";
println!("Match multiline: {}", re_dot.is_match(teks_ml2)); // true
// Batasi ukuran untuk melindungi dari ReDoS
let re_terbatas = RegexBuilder::new(r"\w+")
.size_limit(1024 * 1024) // maks 1MB untuk bytecode regex
.build()
.unwrap();
}
Pola Validasi Siap Pakai #
Kumpulan pola validasi umum yang bisa langsung digunakan:
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
// Email (sederhana — untuk validasi ketat gunakan library khusus)
static ref RE_EMAIL: Regex = Regex::new(
r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$"
).unwrap();
// Nomor telepon Indonesia
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();
// Kode pos Indonesia (5 digit)
static ref RE_KODEPOS: Regex = Regex::new(
r"^\d{5}$"
).unwrap();
// NIK (16 digit)
static ref RE_NIK: Regex = Regex::new(
r"^\d{16}$"
).unwrap();
// URL dengan protokol
static ref RE_URL: Regex = Regex::new(
r"^https?://[\w\-]+(\.[\w\-]+)+([\w\-\._~:/?#\[\]@!\$&'\(\)\*\+,;=%]+)?$"
).unwrap();
// Format tanggal YYYY-MM-DD
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();
// Hanya huruf dan spasi (nama orang)
static ref RE_NAMA: Regex = Regex::new(
r"^[a-zA-Z\s'-]{2,100}$"
).unwrap();
}
fn main() {
let data_uji = [
("Email valid", RE_EMAIL.is_match("[email protected]")),
("Email tidak valid", RE_EMAIL.is_match("bukan@email")),
("Kode pos valid", RE_KODEPOS.is_match("40115")),
("Kode pos tidak", RE_KODEPOS.is_match("4011")),
("URL valid", RE_URL.is_match("https://www.rust-lang.org")),
("Tanggal valid", RE_TANGGAL_ISO.is_match("2024-08-24")),
("Tanggal tidak", RE_TANGGAL_ISO.is_match("2024-13-45")),
];
for (deskripsi, hasil) in &data_uji {
println!("{}: {}", deskripsi, hasil);
}
}
Ringkasan #
- Raw string
r"..."untuk pola regex — menghindari double-escape (\\djadi\d). Selalu gunakan raw string untuk pola regex.- Kompilasi sekali, gunakan berkali-kali —
Regex::newrelatif mahal. Gunakanlazy_static!atauOnceLockuntuk menyimpanRegexsebagai statis global, bukan mengkompilasi di dalam fungsi atau loop.is_matchuntuk validasi,finduntuk posisi,capturesuntuk ekstraksi — pilih method sesuai apa yang dibutuhkan; jangan gunakancapturesjika hanya butuhis_match.- Named capture groups
(?P<nama>...)lebih mudah dibaca — akses dengan&caps["nama"]daripada&caps[1]yang rentan error saat pola berubah.replace_alldengan closure untuk penggantian dinamis — ketika teks pengganti perlu dihitung berdasarkan konten match, gunakan closure sebagai argumenreplace_all.RegexSetuntuk klasifikasi — lebih efisien dari mengecek banyak pola satu per satu; cocok untuk routing, klasifikasi file, dan deteksi format.- Crate
regextidak mendukung lookahead/lookbehind — ini trade-off untuk jaminan O(n). Jika butuh fitur ini, pertimbangkan cratefancy-regex(dengan peringatan: bisa eksponensial).- Selalu tangani error kompilasi —
Regex::newmengembalikanResult; gunakanunwrap()hanya untuk pola yang sudah diverifikasi benar (seperti dilazy_static), atau tangani denganmatchuntuk pola yang datang dari input pengguna.