ZetCode

Regular expressions in Rust

last modified October 25, 2026

In this article we show how to use regular expressions in Rust.

Regular expressions are patterns that describe sets of strings. They are used for searching, extracting, replacing, splitting, and validating text. The Rust standard library does not contain a regex engine; the regex crate, maintained by the Rust project, is the standard choice.

The regex crate guarantees that searching takes time linear in the size of the pattern and the input. To achieve this, it does not support look-around assertions and backreferences, which may need exponential time in backtracking engines. The patterns are Unicode-aware by default.

The examples were tested with Rust 1.97, the 2024 edition, and regex 1.13.1. They are run as Cargo projects with cargo run -q. We add the crate with cargo add regex.

Cargo.toml
[dependencies]
regex = "1.13.1"

The dependency is listed in the Cargo.toml file.

The following table shows common regular expression syntax.

Regex Meaning
. Matches any character except a newline.
? Matches the preceding element once or not at all.
+ Matches the preceding element once or more times.
* Matches the preceding element zero or more times.
{n,m} Matches the preceding element at least n and at most m times.
^ Matches the start of the text (of a line with the m flag).
$ Matches the end of the text (of a line with the m flag).
\b Matches a word boundary.
| Alternation operator.
[abc] Matches a, or b, or c.
[a-c] Range; matches a, or b, or c.
[^abc] Negation; matches any character except a, b, or c.
\d Matches a digit (Unicode decimal number).
\s Matches a whitespace character.
\w Matches a word character: a Unicode letter, digit, or underscore.
\p{Greek} Matches a character with the given Unicode property.
(...) Capturing group.
(?<name>...) Named capturing group.
(?:...) Non-capturing group.
(?i) Case-insensitive flag.

The patterns are written as raw string literals, r"...". In a raw string, the backslash is an ordinary character, so we write r"\d+" instead of "\\d+".

The is_match method

The is_match method tells whether the regular expression finds a match anywhere in the input string.

main.rs
use regex::Regex;

fn main() {
    let words = ["Seven", "even", "Maven", "Amen", "eleven"];
    let rx = Regex::new(r".even").unwrap();

    for word in words {
        if rx.is_match(word) {
            println!("{word} does match");
        } else {
            println!("{word} does not match");
        }
    }
}

In the example, we have five words in an array. We check which words match the .even regular expression.

let rx = Regex::new(r".even").unwrap();

The Regex::new function compiles the pattern. It returns a Result, because the pattern may be invalid. The dot stands for any single character; the rest are ordinary letters.

if rx.is_match(word) {

The method returns true if the pattern matches a part of the word. The word "even" does not match, because there is no character before "even". The word "eleven" matches, because "leven" is a part of it.

$ cargo run -q
Seven does match
even does not match
Maven does not match
Amen does not match
eleven does match

Finding matches

The find method returns the first match, and find_iter returns an iterator over all non-overlapping matches.

main.rs
use regex::Regex;

fn main() {
    let content = "Foxes are omnivorous mammals belonging to several genera
of the family Canidae. Foxes have a flattened skull, upright triangular ears,
a pointed, slightly upturned snout, and a long bushy tail. Foxes live on every
continent except Antarctica. By far the most common and widespread species of
fox is the red fox.";

    let rx = Regex::new(r"(?i)fox(es)?").unwrap();

    for m in rx.find_iter(content) {
        println!("{} at {}..{}", m.as_str(), m.start(), m.end());
    }

    println!("{} matches", rx.find_iter(content).count());

    if let Some(m) = rx.find(content) {
        println!("first match: {:?}", m.range());
    }
}

The example finds all occurrences of "fox" or "foxes" in the text, together with their positions.

let rx = Regex::new(r"(?i)fox(es)?").unwrap();

The (?i) flag makes the search case-insensitive. The (es)? group is optional.

for m in rx.find_iter(content) {
    println!("{} at {}..{}", m.as_str(), m.start(), m.end());
}

Each item is a Match. The as_str method returns the matched text; start and end return the byte offsets in the input.

println!("{} matches", rx.find_iter(content).count());

The count method of the iterator returns the number of matches.

if let Some(m) = rx.find(content) {
    println!("first match: {:?}", m.range());
}

The find method returns an Option<Match>. The range method returns the offsets as a range, which can be used to slice the input.

$ cargo run -q
Foxes at 0..5
Foxes at 80..85
Foxes at 194..199
fox at 292..295
fox at 307..310
5 matches
first match: 0..5

Anchors

Anchors match positions instead of characters. The ^ anchor matches the start of the text, $ its end, and \b a boundary between a word and a non-word character.

main.rs
use regex::Regex;

fn main() {
    let sentences = [
        "I am looking for Jane.",
        "Jane was walking along the river.",
        "Kate and Jane are close friends.",
    ];

    let start = Regex::new(r"^Jane").unwrap();
    let end = Regex::new(r"Jane\.$").unwrap();

    for s in sentences {
        println!("{:<36} start: {:<5} end: {}", s, start.is_match(s), end.is_match(s));
    }

    let word = Regex::new(r"\bis\b").unwrap();
    let text = "This island is beautiful.";
    let found: Vec<usize> = word.find_iter(text).map(|m| m.start()).collect();
    println!("{found:?}");
}

We check whether a sentence starts with "Jane" or ends with "Jane.".

let end = Regex::new(r"Jane\.$").unwrap();

The dot has a special meaning, so it must be escaped to match a literal dot.

let word = Regex::new(r"\bis\b").unwrap();

With word boundaries, the pattern matches only the word "is", not the letters in "This" or "island".

$ cargo run -q
I am looking for Jane.               start: false end: true
Jane was walking along the river.    start: true  end: false
Kate and Jane are close friends.     start: false end: false
[12]

Alternations

The alternation operator | creates a regular expression with several choices.

main.rs
use regex::Regex;

fn main() {
    let users = ["Jane", "Thomas", "Robert", "Lucy", "Beky", "John", "Peter", "Andy"];

    let rx = Regex::new(r"^(Jane|Beky|Robert)$").unwrap();

    let matched: Vec<&str> = users.iter().copied().filter(|u| rx.is_match(u)).collect();
    println!("{matched:?}");

    let loose = Regex::new(r"Jan|Rob").unwrap();
    println!("{}", loose.is_match("Janet"));
    println!("{}", rx.is_match("Janet"));
}

We find the names that are equal to Jane, Beky, or Robert.

let rx = Regex::new(r"^(Jane|Beky|Robert)$").unwrap();

The alternation has the lowest precedence, so it is grouped with parentheses. Without the anchors, the pattern would also match strings that merely contain one of the names.

let loose = Regex::new(r"Jan|Rob").unwrap();
println!("{}", loose.is_match("Janet"));

The pattern without anchors matches "Janet", because it contains "Jan".

$ cargo run -q
["Jane", "Robert", "Beky"]
true
false

Character classes and quantifiers

Character classes match one character from a set; quantifiers specify how many times the preceding element repeats.

main.rs
use regex::Regex;

fn main() {
    let text = "The year 1998 was hot; in 2003 and 2019 too. Call 555-1234 or 555-98765.";

    let years = Regex::new(r"\b(19|20)\d{2}\b").unwrap();
    let ys: Vec<&str> = years.find_iter(text).map(|m| m.as_str()).collect();
    println!("years: {ys:?}");

    let phone = Regex::new(r"\b\d{3}-\d{4}\b").unwrap();
    let ps: Vec<&str> = phone.find_iter(text).map(|m| m.as_str()).collect();
    println!("phones: {ps:?}");

    let words = Regex::new(r"\b[a-z]{3,4}\b").unwrap();
    let ws: Vec<&str> = words.find_iter(text).map(|m| m.as_str()).collect();
    println!("short words: {ws:?}");

    let not_word = Regex::new(r"[^\w\s]").unwrap();
    let ns: Vec<&str> = not_word.find_iter(text).map(|m| m.as_str()).collect();
    println!("punctuation: {ns:?}");
}

We find years, phone numbers, short words, and punctuation characters.

let years = Regex::new(r"\b(19|20)\d{2}\b").unwrap();

A year starts with 19 or 20 and is followed by exactly two digits.

let phone = Regex::new(r"\b\d{3}-\d{4}\b").unwrap();

The pattern matches three digits, a hyphen, and four digits. The number 555-98765 does not match, because the word boundary after four digits is missing.

let words = Regex::new(r"\b[a-z]{3,4}\b").unwrap();

The {3,4} quantifier matches words of three or four lowercase letters.

let not_word = Regex::new(r"[^\w\s]").unwrap();

A negated class matches any character that is neither a word character nor a whitespace.

$ cargo run -q
years: ["1998", "2003", "2019"]
phones: ["555-1234"]
short words: ["year", "was", "hot", "and", "too"]
punctuation: [";", ".", "-", "-", "."]

Greedy and lazy quantifiers

The quantifiers are greedy: they match as much text as possible. Adding a question mark makes them lazy, so that they match as little as possible.

main.rs
use regex::Regex;

fn main() {
    let html = "<p>first</p><p>second</p>";

    let greedy = Regex::new(r"<p>.*</p>").unwrap();
    let lazy = Regex::new(r"<p>.*?</p>").unwrap();

    for m in greedy.find_iter(html) {
        println!("greedy: {}", m.as_str());
    }

    for m in lazy.find_iter(html) {
        println!("lazy: {}", m.as_str());
    }
}

We match HTML paragraphs with a greedy and a lazy quantifier.

let greedy = Regex::new(r"<p>.*</p>").unwrap();
let lazy = Regex::new(r"<p>.*?</p>").unwrap();

The greedy .* runs to the last </p>, so it matches both paragraphs at once. The lazy .*? stops at the first </p>. Regular expressions are fine for simple fragments like this; for real HTML documents, use an HTML parser.

$ cargo run -q
greedy: <p>first</p><p>second</p>
lazy: <p>first</p>
lazy: <p>second</p>

Capturing groups

Round brackets create capturing groups. The captures method returns the text matched by the whole pattern and by each group.

main.rs
use regex::Regex;

fn main() {
    let sites = ["example.com", "zetcode.com", "freebsd.org", "rust-lang.org", "localhost"];

    let rx = Regex::new(r"^([\w-]+)\.(\w+)$").unwrap();

    for site in sites {
        match rx.captures(site) {
            Some(caps) => println!("{:<14} name: {:<10} domain: {}", &caps[0], &caps[1], &caps[2]),
            None => println!("{site:<14} no match"),
        }
    }
}

In the example, we divide domain names into two parts.

let rx = Regex::new(r"^([\w-]+)\.(\w+)$").unwrap();

The pattern has two groups: the name, which may contain hyphens, and the top-level domain.

match rx.captures(site) {
    Some(caps) => println!("{:<14} name: {:<10} domain: {}", &caps[0], &caps[1], &caps[2]),
    None => println!("{site:<14} no match"),
}

The captures method returns None if the pattern does not match. The group at the index 0 is the whole match; the groups are numbered from 1 by their opening parentheses. Indexing panics if the group did not participate in the match.

$ cargo run -q
example.com    name: example    domain: com
zetcode.com    name: zetcode    domain: com
freebsd.org    name: freebsd    domain: org
rust-lang.org  name: rust-lang  domain: org
localhost      no match

Named groups

Groups can have names, which make the patterns easier to read. The captures_iter method returns the captures for every match.

main.rs
use regex::Regex;

fn main() {
    let text = "Released on 2024-03-15, patched on 2024-11-02 and 2025-01-20.";
    let rx = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})").unwrap();

    for caps in rx.captures_iter(text) {
        let year: u32 = caps["year"].parse().unwrap();
        println!("day {} month {} year {year}", &caps["day"], &caps["month"]);
    }

    for (_, [y, m, d]) in rx.captures_iter(text).map(|c| c.extract()) {
        println!("{d}.{m}.{y}");
    }

    let caps = rx.captures("no date here");
    println!("{caps:?}");
}

We extract dates from a text.

let rx = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})").unwrap();

The (?<name>...) syntax defines named groups. The (?P<name>...) syntax known from Python is supported too.

let year: u32 = caps["year"].parse().unwrap();

Named groups are accessed by indexing with the group name. The captured text is a string slice; we parse it into a number.

for (_, [y, m, d]) in rx.captures_iter(text).map(|c| c.extract()) {
    println!("{d}.{m}.{y}");
}

The extract method returns the whole match and an array of the groups. The size of the array is checked when the program runs; it must be equal to the number of groups, and all groups must participate in the match.

$ cargo run -q
day 15 month 03 year 2024
day 02 month 11 year 2024
day 20 month 01 year 2025
15.03.2024
02.11.2024
20.01.2025
None

Optional groups

An optional group may not participate in a match. The get method returns an Option<Match> for a group, so it handles this case without panicking.

main.rs
use regex::Regex;

fn main() {
    let rx = Regex::new(r"(\d+)(?:\.(\d+))?\s*(kg|g)").unwrap();

    for s in ["12 kg", "3.5kg", "250 g", "7 lb"] {
        let Some(caps) = rx.captures(s) else {
            println!("{s}: no match");
            continue;
        };

        let whole = &caps[1];
        let frac = caps.get(2).map_or("0", |m| m.as_str());
        let unit = &caps[3];

        println!("{s}: whole {whole}, fraction {frac}, unit {unit}");
    }
}

We parse weights with an optional fractional part.

let rx = Regex::new(r"(\d+)(?:\.(\d+))?\s*(kg|g)").unwrap();

The (?:...) group is non-capturing; it only groups the dot with the digits, so that the ? applies to both. The inner group with the digits is the second capturing group.

let frac = caps.get(2).map_or("0", |m| m.as_str());

For "12 kg", the second group did not participate in the match; get returns None and we use a default value.

$ cargo run -q
12 kg: whole 12, fraction 0, unit kg
3.5kg: whole 3, fraction 5, unit kg
250 g: whole 250, fraction 0, unit g
7 lb: no match

Replacing text

The replace method replaces the first match and replace_all replaces all matches. The replacement string can refer to the groups.

main.rs
use regex::Regex;

fn main() {
    let text = "My name is John Doe. John lives in London.";

    let rx = Regex::new(r"John").unwrap();
    println!("{}", rx.replace(text, "Jane"));
    println!("{}", rx.replace_all(text, "Jane"));

    let names = Regex::new(r"(?<first>\w+) (?<last>Doe)").unwrap();
    println!("{}", names.replace(text, "$last, $first"));

    let spaces = Regex::new(r"\s+").unwrap();
    println!("{}", spaces.replace_all("an   old \t falcon", " "));

    let ids = Regex::new(r"(\d+)").unwrap();
    println!("{}", ids.replace_all("item 7, item 12", "${1}a"));
}

The example shows several kinds of replacements.

println!("{}", rx.replace(text, "Jane"));
println!("{}", rx.replace_all(text, "Jane"));

The methods return a Cow<str>. If there is no match, the input is returned without copying.

let names = Regex::new(r"(?<first>\w+) (?<last>Doe)").unwrap();
println!("{}", names.replace(text, "$last, $first"));

In the replacement string, $name or $1 refers to a group. We swap the first and the last name.

println!("{}", ids.replace_all("item 7, item 12", "${1}a"));

When a group reference is followed by a letter or a digit, it must be enclosed in braces. The $1a would refer to a group named 1a. A literal dollar sign is written as $$.

$ cargo run -q
My name is Jane Doe. John lives in London.
My name is Jane Doe. Jane lives in London.
My name is Doe, John. John lives in London.
an old falcon
item 7a, item 12a

Replacing with a closure

The replacement can be computed by a closure, which receives the captures of each match.

main.rs
use regex::{Captures, Regex};

fn main() {
    let text = "3 apples cost 120 cents, 12 plums cost 300 cents.";

    let rx = Regex::new(r"(\d+) cents").unwrap();
    let res = rx.replace_all(text, |caps: &Captures| {
        let cents: u32 = caps[1].parse().unwrap();
        format!("${}.{:02}", cents / 100, cents % 100)
    });
    println!("{res}");

    let words = Regex::new(r"\b\w").unwrap();
    let title = words.replace_all("the old man and the sea", |caps: &Captures| caps[0].to_uppercase());
    println!("{title}");
}

We convert prices in cents into dollars and capitalize the words of a title.

let res = rx.replace_all(text, |caps: &Captures| {
    let cents: u32 = caps[1].parse().unwrap();
    format!("${}.{:02}", cents / 100, cents % 100)
});

The closure returns the replacement as a String. Unlike in a replacement string, the dollar sign in the closure output is a literal character.

let words = Regex::new(r"\b\w").unwrap();

The pattern matches the first character of every word; the closure turns it into uppercase.

$ cargo run -q
3 apples cost $1.20, 12 plums cost $3.00.
The Old Man And The Sea

Splitting text

The split method splits the text at the matches of the pattern; splitn limits the number of parts.

main.rs
use regex::Regex;

fn main() {
    let text = "sky, blue;cloud ;  rain,,snow";
    let rx = Regex::new(r"\s*[,;]\s*").unwrap();

    let parts: Vec<&str> = rx.split(text).collect();
    println!("{parts:?}");

    let parts: Vec<&str> = rx.split(text).filter(|s| !s.is_empty()).collect();
    println!("{parts:?}");

    let parts: Vec<&str> = rx.splitn(text, 2).collect();
    println!("{parts:?}");
}

We split a list whose items are separated with commas or semicolons and an arbitrary amount of whitespace.

let rx = Regex::new(r"\s*[,;]\s*").unwrap();

The separator is a comma or a semicolon with optional whitespace around it.

let parts: Vec<&str> = rx.split(text).filter(|s| !s.is_empty()).collect();

Two separators in a row produce an empty string, which we filter out.

let parts: Vec<&str> = rx.splitn(text, 2).collect();

With splitn, the last part contains the rest of the text.

$ cargo run -q
["sky", "blue", "cloud", "rain", "", "snow"]
["sky", "blue", "cloud", "rain", "snow"]
["sky", "blue;cloud ;  rain,,snow"]

Flags

Flags change how the pattern is matched. They are written inside the pattern with the (?flags) syntax or set with RegexBuilder.

main.rs
use regex::{Regex, RegexBuilder};

fn main() {
    let text = "Rust is fast.\nrust is safe.\nRUST is fun.";

    let rx = Regex::new(r"(?im)^rust").unwrap();
    println!("{}", rx.find_iter(text).count());

    let rx = Regex::new(r"^rust").unwrap();
    println!("{}", rx.find_iter(text).count());

    let rx = RegexBuilder::new(r"^rust")
        .case_insensitive(true)
        .multi_line(true)
        .build()
        .unwrap();
    println!("{}", rx.find_iter(text).count());

    let rx = Regex::new(r"(?s)fast.*safe").unwrap();
    println!("{}", rx.is_match(text));

    let date = Regex::new(
        r"(?x)
        (?<y>\d{4})  # the year
        -
        (?<m>\d{2})  # the month
        ",
    )
    .unwrap();
    println!("{}", &date.captures("in 2026-10").unwrap()["m"]);
}

We count the lines that start with "rust" in any case.

let rx = Regex::new(r"(?im)^rust").unwrap();

The i flag enables case-insensitive matching; the m flag makes ^ and $ match at the start and end of each line. Without the flags, there is no match, because the text starts with "Rust".

let rx = RegexBuilder::new(r"^rust")
    .case_insensitive(true)
    .multi_line(true)
    .build()
    .unwrap();

The builder sets the same flags with methods.

let rx = Regex::new(r"(?s)fast.*safe").unwrap();

With the s flag, the dot also matches the newline, so the pattern can match across lines.

let date = Regex::new(
    r"(?x)
    (?<y>\d{4})  # the year
    -
    (?<m>\d{2})  # the month
    ",
)

The x flag enables the verbose mode: whitespace in the pattern is ignored and # starts a comment. Complex patterns become easier to read.

$ cargo run -q
3
0
3
true
10

Validating input

To check that a whole string has a given form, the pattern is anchored at both ends.

main.rs
use regex::Regex;

fn main() {
    let rx = Regex::new(r"^[\w.+-]+@[\w-]+(\.[\w-]+)*\.[a-z]{2,}$").unwrap();

    let emails = [
        "luke@gmail.com",
        "andy@yahoocom",
        "34234sdfa#2345",
        "f344@gmail.com",
        "jane.doe+news@mail.example.org",
        "@example.com",
    ];

    for email in emails {
        let status = if rx.is_match(email) { "valid" } else { "invalid" };
        println!("{email:<32} {status}");
    }
}

We check a list of email addresses.

let rx = Regex::new(r"^[\w.+-]+@[\w-]+(\.[\w-]+)*\.[a-z]{2,}$").unwrap();

The local part consists of word characters, dots, pluses, and hyphens. The domain has one or more parts separated by dots and ends with a top-level domain of at least two letters. The pattern is a simplification; the full syntax of email addresses is much more complex, and the only reliable check is to send an email.

$ cargo run -q
luke@gmail.com                   valid
andy@yahoocom                    invalid
34234sdfa#2345                   invalid
f344@gmail.com                   valid
jane.doe+news@mail.example.org   valid
@example.com                     invalid

Unicode

The classes \w, \d, and \s are Unicode-aware. Unicode properties are matched with \p{...}.

main.rs
use regex::Regex;

fn main() {
    let text = "Žltý kôň, 東京 and Αθήνα; café ŁÓDŹ";

    let words = Regex::new(r"\w+").unwrap();
    let ws: Vec<&str> = words.find_iter(text).map(|m| m.as_str()).collect();
    println!("{ws:?}");

    let ascii = Regex::new(r"(?-u:\w)+").unwrap();
    let ascii_ws: Vec<&str> = ascii.find_iter(text).map(|m| m.as_str()).collect();
    println!("{ascii_ws:?}");

    let greek = Regex::new(r"\p{Greek}+").unwrap();
    println!("{:?}", greek.find(text).map(|m| m.as_str()));

    let han = Regex::new(r"\p{Han}+").unwrap();
    println!("{:?}", han.find(text).map(|m| m.as_str()));

    let upper = Regex::new(r"\b\p{Lu}+\b").unwrap();
    println!("{:?}", upper.find(text).map(|m| m.as_str()));
}

We find words in a text that contains Slovak, Japanese, Greek, French, and Polish words.

let words = Regex::new(r"\w+").unwrap();

The \w class matches letters of all alphabets.

let ascii = Regex::new(r"(?-u:\w)+").unwrap();

The (?-u:...) group disables the Unicode mode; the class then matches only [0-9A-Za-z_] and splits the words with accented letters.

let greek = Regex::new(r"\p{Greek}+").unwrap();
let han = Regex::new(r"\p{Han}+").unwrap();
let upper = Regex::new(r"\b\p{Lu}+\b").unwrap();

The \p{Greek} and \p{Han} classes match scripts; \p{Lu} matches uppercase letters.

$ cargo run -q
["Žltý", "kôň", "東京", "and", "Αθήνα", "café", "ŁÓDŹ"]
["lt", "k", "and", "caf", "D"]
Some("Αθήνα")
Some("東京")
Some("ŁÓDŹ")

Regex sets

A RegexSet matches several patterns in a single pass over the input and reports which of them matched.

main.rs
use regex::RegexSet;

fn main() {
    let set = RegexSet::new([
        r"^\d+$",
        r"^[a-z]+$",
        r"^[A-Z]",
        r"\s",
    ])
    .unwrap();

    let names = ["digits", "lowercase", "capitalized", "has space"];

    for s in ["2026", "falcon", "Rust", "an old Falcon"] {
        let hits: Vec<&str> = set.matches(s).into_iter().map(|i| names[i]).collect();
        println!("{s:<14} {hits:?}");
    }
}

We classify strings with four patterns.

let hits: Vec<&str> = set.matches(s).into_iter().map(|i| names[i]).collect();

The matches method returns the indexes of the patterns that matched. A set does not report the positions of the matches; for these, we use the individual regexes.

$ cargo run -q
2026           ["digits"]
falcon         ["lowercase"]
Rust           ["capitalized"]
an old Falcon  ["has space"]

Compiling a regex once

Compiling a regular expression is much slower than using it. A regex used in a function that is called many times should be compiled only once. A LazyLock static compiles it on first use.

main.rs
use regex::Regex;
use std::sync::LazyLock;

static WORD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\b[A-Z]\w*").unwrap());

fn capitalized(text: &str) -> Vec<&str> {
    WORD.find_iter(text).map(|m| m.as_str()).collect()
}

fn main() {
    let lines = [
        "Alice met Bob in Paris.",
        "the weather was fine.",
        "They visited the Louvre and Notre-Dame.",
    ];

    for line in lines {
        println!("{:?}", capitalized(line));
    }
}

We find capitalized words in several lines.

static WORD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\b[A-Z]\w*").unwrap());

The LazyLock type from the standard library runs the closure the first time the static is accessed. The Regex can be shared between threads, so it can be used from a static.

fn capitalized(text: &str) -> Vec<&str> {
    WORD.find_iter(text).map(|m| m.as_str()).collect()
}

The function returns slices of the input text. "Notre-Dame" is split into two matches, because the hyphen is not a word character.

$ cargo run -q
["Alice", "Bob", "Paris"]
[]
["They", "Louvre", "Notre", "Dame"]

Invalid patterns

An invalid pattern is reported as an error by Regex::new. When a pattern comes from the user, the error must be handled.

main.rs
use regex::Regex;

fn main() {
    for pattern in [r"\d+", r"(\d+", r"[z-a]", r"\p{Foo}"] {
        match Regex::new(pattern) {
            Ok(rx) => println!("ok: {rx}"),
            Err(e) => println!("{e}\n"),
        }
    }
}

We try to compile one valid and three invalid patterns.

match Regex::new(pattern) {
    Ok(rx) => println!("ok: {rx}"),
    Err(e) => println!("{e}\n"),
}

The error message shows the pattern and marks the position of the problem. A regex printed with {} displays its pattern.

$ cargo run -q
ok: \d+
regex parse error:
    (\d+
    ^
error: unclosed group

regex parse error:
    [z-a]
     ^^^
error: invalid character class range, the start must be <= the end

regex parse error:
    \p{Foo}
    ^^^^^^^
error: Unicode property not found

Source

The regex crate documentation

The regex syntax

In this article we have used regular expressions in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.