Regular expressions with regex
A regular expression is a pattern compiled into a matcher, and the regex crate is the one Rust programs use. The pattern is compiled once, the matcher is cheap to run, and the engine is guaranteed to stay linear in the size of the input, which is why it has no backreferences.
This lesson finds every date in a line, parses a log line into named groups, shows a pattern that does not match and one that does not compile, and splits and replaces text. The crate is at version 1.13.1, which the manifest names, and the last section is the compiler refusing a regex method that belongs to the crate rather than to the string.
A pattern and its matches
Regex::new compiles the pattern and returns a Result, because a pattern is data and can be wrong. A raw string keeps the backslashes of the pattern out of the Rust escapes, which is why every regex in Rust is written with r"...".
The program searches a sentence for dates and collects the text of every match. Two dates are found, in the order they appear.
use regex::Regex;
fn main() {
let pattern = Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap();
let text = "released 2024-03-01 and fixed 2024-04-15";
let dates: Vec<&str> = pattern.find_iter(text).map(|m| m.as_str()).collect();
println!("{dates:?}");
}
cargo run
Compiling regex v1.13.1
Compiling scan v0.1.0 (/tmp/rw8/L106/scan)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 25.60s
Running `target/debug/scan`
["2024-03-01", "2024-04-15"]
find_iter walks the matches without building a vector of them, which is the lazy shape of the iterator lesson. An unmatched text returns an empty iterator instead of an error, because a search that finds nothing is a normal result. The dependency lesson is the manifest line above.
Named captures
Parentheses group part of a pattern, and (?P<name>...) gives the group a name that the code can use instead of a number. captures returns Option<Captures>, and the index operator reaches a group by name.
The program parses a log line into a level, a code and a message. The three groups are printed with the separators removed.
use regex::Regex;
fn main() {
let pattern = Regex::new(r"^(?P<level>\w+) +(?P<code>\d{3}) +(?P<message>.+)$").unwrap();
let line = "WARN 404 file not found";
let captures = pattern.captures(line).unwrap();
println!("{} {} {}", &captures["level"], &captures["code"], &captures["message"]);
}
cargo run
Compiling scan v0.1.0 (/tmp/rw8/L106/scan)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s
Running `target/debug/scan`
WARN 404 file not found
The anchors are what make the parse total: without ^ and $ the pattern would match a part of a longer line and the message would be cut. The \w+ and \d{3} are the shorthand classes, and a space followed by + collapses any run of them.
No match, and an invalid pattern
A pattern that does not match returns None from captures, which a match turns into a message. A pattern that is not valid returns an Err from Regex::new, and the error prints the position of the problem in the pattern.
The program tests a line with three numbers against a pattern for two, then compiles a pattern with an unclosed group. The second part prints the parse error.
use regex::Regex;
fn main() {
let pattern = Regex::new(r"^(\d+)-(\d+)$").unwrap();
println!("{}", pattern.captures("12-34-56").is_none());
match Regex::new(r"(\d+") {
Ok(_) => println!("valid"),
Err(error) => println!("{error}"),
}
}
cargo run
Compiling scan v0.1.0 (/tmp/rw8/L106/scan)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.92s
Running `target/debug/scan`
true
regex parse error:
(\d+
^
error: unclosed group
The caret points at the open parenthesis that was never closed, which is the best a compiler of patterns can do. A program that reads patterns from a configuration file has to handle this error, because the file is not written by the program. The lesson on error types is the shape of that handling.
Split and replace
A pattern is also a separator and a thing to replace. split cuts the text wherever the pattern matches, and replace_all rewrites every match with a replacement that may refer to groups.
The program trims a line of words, splits it on runs of whitespace and collapses the gaps in another string to a single underscore.
use regex::Regex;
fn main() {
let pattern = Regex::new(r"\s+").unwrap();
let text = " alpha beta gamma ";
let words: Vec<&str> = pattern.split(text.trim()).collect();
println!("{words:?}");
println!("{}", pattern.replace_all("a b c", "_"));
}
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/scan`
["alpha", "beta", "gamma"]
a_b_c
The trim is what keeps an empty first word out of the result, and the pattern handles any number of spaces in one match. A replacement can name a group as $name, which is how a date is rewritten from one order to another.
The method that belongs to the crate
The matching methods belong to Regex and not to &str, so a call on a string is a missing method. The compiler reports E0599 and the fix is to compile the pattern first.
The program asks a string whether it matches a pattern. The method does not exist on the string type.
fn main() {
let text = "abc123";
println!("{}", text.is_match(r"\d+"));
}
cargo build
Compiling scan v0.1.0 (/tmp/rw8/L106/scan)
error[E0599]: no method named `is_match` found for reference `&str` in the current scope
--> src/main.rs:3:25
|
3 | println!("{}", text.is_match(r"\d+"));
| ^^^^^^^^ method not found in `&str`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `scan` (bin "scan") due to 1 previous error
The fix is two lines: compile the pattern with Regex::new and call is_match on it. The same shape appears with every string method that takes a pattern: contains and split exist on the string for a character or a literal, and a regular expression is always a compiled object. The next lesson reads the text from the command line.
Exercises
- Write a pattern for an email address, run it over three strings and print the ones that match.
- Rewrite every date from 2024-03-01 to 01.03.2024 with replace_all and group references.
- Compile a pattern from a string the user provides and print the parse error for a bad one.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
Regex::newreturn?Answer
A Result, because the pattern can be invalid
A pattern is data and is checked when it is compiled.
-
Why is a pattern written as a raw string?
Answer
It keeps the backslashes of the pattern from being read as Rust escapes
In a normal string a backslash starts an escape, and a regex is full of them.
-
What does a named group look like in a pattern?
Answer
(?P<name>...)
The ?P prefix names the group, and the code reads it by that name.
-
What does
capturesreturn when the pattern does not match?Answer
None
The Option is the answer to a search that found nothing.
-
What does this program print?
fn main() { let text = "a1b2c3"; let digits: String = text.chars().filter(|c| c.is_ascii_digit()).collect(); println!("{digits}"); }Answer
123
The filter keeps the three digits and collect builds the string.
-
What does this program print?
fn main() { let text = "2024-03-01"; let parts: Vec<&str> = text.split('-').collect(); println!("{} {}", parts.len(), parts[1]); }Answer
3 03
Two dashes divide the date into three parts, and the second is the month.
-
What does this program print?
fn main() { let text = " alpha beta "; let words: Vec<&str> = text.split_whitespace().collect(); println!("{words:?}"); }Answer
["alpha", "beta"]
The method treats any run of whitespace as one separator and skips the ends.
-
What does this program print?
fn main() { let text = "x=1, y=22, z=333"; let sum: i32 = text .split(", ") .map(|part| part.split('=').nth(1).unwrap().parse::<i32>().unwrap()) .sum(); println!("{sum}"); }Answer
356
One plus twenty-two plus three hundred and thirty-three is three hundred and fifty-six.
-
What is the first line of the compiler output for this program?
fn main() { let text = "abc123"; println!("{}", text.is_match(r"\d+")); }Answer
error[E0599]: no method named
is_matchfound for reference&strin the current scopeThe method belongs to Regex, and a string has no such method.
-
What is the first line of the compiler output for this program?
fn main() { let pattern = r"\d+"; let text = "abc123"; println!("{}", pattern.is_match(text)); }Answer
error[E0599]: no method named
is_matchfound for reference&strin the current scopeA raw string is still a string, so it has no matching method either.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1, with regex 1.13.1 from crates.io, and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev