Skip to content

Commit e358341

Browse files
tgross35BurntSushi
authored andcommitted
api: add regex! macro for lazy compilation
Add a wrapper around `regex_automata`'s `Lazy` as a simple way to construct a `Regex` that is compiled once but used multiple times. Sample usage: if regex!(r"\d+").is_match("123") { /* ... */ } let re: &Regex = regex!(r"\d+"); This idea has been discussed in #709. To address a few of the concerns from that issue: 1. More than 1 `regex!` can be used in a block (for the anonymous version, the name is in a scope). 2. Using `regex_automata`'s `Lazy` means there are no MSRV concerns, and this works without std. 3. There is no compile-time checking. The docs make it clear that `regex!` should not be used with invalid regex and suggest enabling the clippy lint, leaving the door somewhat open for e.g. a `const fn` syntax validator in the future (though this seems unlikely). A `regex::bytes::regex!` version is also included. Closes #709
1 parent c420333 commit e358341

7 files changed

Lines changed: 202 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
1-
1.12.4 (2025-06-09)
1+
1.13.0 (2026-07-09)
2+
===================
3+
This release includes a new API, a `regex!` macro, for lazy compilation of
4+
a regex from a string literal. If you use regexes a lot, it's likely you've
5+
already written one exactly like it. The new macro can be used like this:
6+
7+
```rust
8+
use regex::regex;
9+
10+
fn is_match(line: &str) -> bool {
11+
// The regex will be compiled approximately once and reused automatically.
12+
// This avoids the footgun of using `Regex::new` here, which would
13+
// guarantee that it would be compiled every time this routine is called.
14+
// This would likely make this routine much slower than it needs to be.
15+
regex!(r"bar|baz").is_match(line)
16+
}
17+
18+
let hay = "\
19+
path/to/foo:54:Blue Harvest
20+
path/to/bar:90:Something, Something, Something, Dark Side
21+
path/to/baz:3:It's a Trap!
22+
";
23+
24+
let matches = hay.lines().filter(|line| is_match(line)).count();
25+
assert_eq!(matches, 2);
26+
```
27+
28+
Improvements:
29+
30+
* [#709](https://github.com/rust-lang/regex/issues/709):
31+
Add a new `regex!` macro for efficient and automatic reuse of a compiled regex.
32+
33+
34+
1.12.4 (2026-06-09)
235
===================
336
This release includes a performance optimization for compilation of regexes
437
with very large character classes.
@@ -9,7 +42,7 @@ Improvements:
942
Avoid re-canonicalizing the entire interval set when pushing new class ranges.
1043

1144

12-
1.12.3 (2025-02-03)
45+
1.12.3 (2026-02-03)
1346
===================
1447
This release excludes some unnecessary things from the archive published to
1548
crates.io. Specifically, fuzzing data and various shell scripts are now

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,11 @@ fn main() {
105105
Specifically, in this example, the regex will be compiled when it is used for
106106
the first time. On subsequent uses, it will reuse the previous compilation.
107107

108+
The [`regex!`] macro can also be used, which handles lazy compilation.
109+
108110
[`std::sync::LazyLock`]: https://doc.rust-lang.org/std/sync/struct.LazyLock.html
109111
[`once_cell`]: https://crates.io/crates/once_cell
112+
[`regex!`]: https://docs.rs/regex/*/regex/macro.regex.html
110113

111114
### Usage: match regular expressions on `&[u8]`
112115

src/bytes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,7 @@ In general, one should expect performance on `&[u8]` to be roughly similar to
8989
performance on `&str`.
9090
*/
9191
pub use crate::{builders::bytes::*, regex::bytes::*, regexset::bytes::*};
92+
93+
// Re-export the public but hidden macro to make it usable as `bytes::regex`.
94+
#[doc(inline)]
95+
pub use crate::__bytes_regex as regex;

src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ assert_eq!(results, vec![
3535
# Ok::<(), Box<dyn std::error::Error>>(())
3636
```
3737
38+
Or, make use of the [`regex!`](crate::regex) macro to compile the regex once
39+
and re-use that same regex automatically. This is useful inside functions.
40+
For example:
41+
42+
```rust
43+
use regex::regex;
44+
45+
fn is_match(line: &str) -> bool {
46+
// The regex will be compiled approximately once and reused automatically.
47+
// This avoids the footgun of using `Regex::new` here, which would
48+
// guarantee that it would be compiled every time this routine is called.
49+
// This would likely make this routine much slower than it needs to be.
50+
regex!(r"bar|baz").is_match(line)
51+
}
52+
53+
let hay = "\
54+
path/to/foo:54:Blue Harvest
55+
path/to/bar:90:Something, Something, Something, Dark Side
56+
path/to/baz:3:It's a Trap!
57+
";
58+
59+
let matches = hay.lines().filter(|line| is_match(line)).count();
60+
assert_eq!(matches, 2);
61+
```
62+
3863
# Overview
3964
4065
The primary type in this crate is a [`Regex`]. Its most important methods are
@@ -498,6 +523,8 @@ Specifically, in this example, the regex will be compiled when it is used for
498523
the first time. On subsequent uses, it will reuse the previously built `Regex`.
499524
Notice how one can define the `Regex` locally to a specific function.
500525
526+
The [`regex!`] macro can also be used, which handles lazy compilation.
527+
501528
[`std::sync::LazyLock`]: https://doc.rust-lang.org/std/sync/struct.LazyLock.html
502529
[`once_cell`]: https://crates.io/crates/once_cell
503530
@@ -1351,3 +1378,9 @@ mod regexset;
13511378
pub fn escape(pattern: &str) -> alloc::string::String {
13521379
regex_syntax::escape(pattern)
13531380
}
1381+
1382+
/// Public-but-unstable API for macro support.
1383+
#[doc(hidden)]
1384+
pub mod __private {
1385+
pub use regex_automata::util::lazy::Lazy;
1386+
}

src/regex/bytes.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,59 @@ use regex_automata::{meta, util::captures, Input, PatternID};
44

55
use crate::{bytes::RegexBuilder, error::Error};
66

7+
/// A convenient way to construct regex patterns from string literals.
8+
///
9+
/// This macro can be used to construct reusable instances of [`Regex`] with
10+
/// reduced boilerplate. The constructed `Regex` is stored in a static so the
11+
/// pattern is compiled approximately once, even when called multiple times.
12+
///
13+
/// There is *no compile-time checking of patterns* with `regex!`. Instead,
14+
/// invalid patterns will panic the first time the regex is used. Invalid
15+
/// patterns should still not be used with `regex!`; if compile-time checking
16+
/// becomes feasible in the future, it may be added within a non-semver-breaking
17+
/// release. In the meantime, consider enabling [`clippy::invalid_regex`].
18+
///
19+
/// # Examples
20+
///
21+
/// ```
22+
/// use regex::bytes::{Regex, regex};
23+
///
24+
/// assert!(regex!("[a-z]").is_match(b"a"));
25+
/// assert!(regex!("(inconceivable!|classic blunder)").is_match(b"inconceivable!"));
26+
///
27+
/// let re: &Regex = regex!(r"(\d{3})-(\d{4})");
28+
/// assert_eq!(&re.captures(b"867-5309").unwrap()[1], b"867");
29+
/// ```
30+
///
31+
/// An invalid pattern will panic when it is first used:
32+
///
33+
/// ```should_panic
34+
/// use regex::bytes::regex;
35+
///
36+
/// let re = regex!("invalid -> ("); // no panic here
37+
/// re.is_match(b"invalid -> ("); // panic!
38+
/// ```
39+
///
40+
/// [`clippy::invalid_regex`]: https://rust-lang.github.io/rust-clippy/master/#invalid_regex
41+
// `macro_export` always makes the macro available at crate root. We hide
42+
// from documentation there and instead re-export it in `crate::bytes` to get
43+
// the desired behavior.
44+
#[macro_export]
45+
#[doc(hidden)]
46+
macro_rules! __bytes_regex {
47+
($re:literal) => {{
48+
static REGEX: $crate::__private::Lazy<$crate::bytes::Regex> =
49+
$crate::__private::Lazy::new(|| {
50+
$crate::bytes::Regex::new($re).expect("invalid regex pattern")
51+
});
52+
53+
// Coerce returned type from `&Lazy<Regex>` to `&Regex` to avoid making the
54+
// inner type public.
55+
let re: &$crate::bytes::Regex = &REGEX;
56+
re
57+
}};
58+
}
59+
760
/// A compiled regular expression for searching Unicode haystacks.
861
///
962
/// A `Regex` can be used to search haystacks, split haystacks into substrings

src/regex/string.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,55 @@ use regex_automata::{meta, util::captures, Input, PatternID};
44

55
use crate::{error::Error, RegexBuilder};
66

7+
/// A convenient way to construct regex patterns from string literals.
8+
///
9+
/// This macro can be used to construct reusable instances of [`Regex`] with
10+
/// reduced boilerplate. The constructed `Regex` is stored in a static so the
11+
/// pattern is compiled approximately once, even when called multiple times.
12+
///
13+
/// There is *no compile-time checking of patterns* with `regex!`. Instead,
14+
/// invalid patterns will panic the first time the regex is used. Invalid
15+
/// patterns should still not be used with `regex!`; if compile-time checking
16+
/// becomes feasible in the future, it may be added within a non-semver-breaking
17+
/// release. In the meantime, consider enabling [`clippy::invalid_regex`].
18+
///
19+
/// # Examples
20+
///
21+
/// ```
22+
/// use regex::{Regex, regex};
23+
///
24+
/// assert!(regex!("[a-z]").is_match("a"));
25+
/// assert!(regex!("(inconceivable!|classic blunder)").is_match("inconceivable!"));
26+
///
27+
/// let re: &Regex = regex!(r"(\d{3})-(\d{4})");
28+
/// assert_eq!(&re.captures("867-5309").unwrap()[1], "867");
29+
/// ```
30+
///
31+
/// An invalid pattern will panic when it is first used:
32+
///
33+
/// ```should_panic
34+
/// use regex::regex;
35+
///
36+
/// let re = regex!("invalid -> ("); // no panic here
37+
/// re.is_match("invalid -> ("); // panic!
38+
/// ```
39+
///
40+
/// [`clippy::invalid_regex`]: https://rust-lang.github.io/rust-clippy/master/#invalid_regex
41+
#[macro_export]
42+
macro_rules! regex {
43+
($re:literal) => {{
44+
static REGEX: $crate::__private::Lazy<$crate::Regex> =
45+
$crate::__private::Lazy::new(|| {
46+
$crate::Regex::new($re).expect("invalid regex pattern")
47+
});
48+
49+
// Coerce returned type from `&Lazy<Regex>` to `&Regex` to avoid making the
50+
// inner type public.
51+
let re: &$crate::Regex = &REGEX;
52+
re
53+
}};
54+
}
55+
756
/// A compiled regular expression for searching Unicode haystacks.
857
///
958
/// A `Regex` can be used to search haystacks, split haystacks into substrings

tests/misc.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
use regex::Regex;
2-
3-
macro_rules! regex {
4-
($pattern:expr) => {
5-
regex::Regex::new($pattern).unwrap()
6-
};
7-
}
1+
use regex::{bytes, regex, Regex};
82

93
#[test]
104
fn unclosed_group_error() {
@@ -141,3 +135,27 @@ fn dfa_handles_pathological_case() {
141135
};
142136
assert!(re.is_match(&text));
143137
}
138+
139+
#[test]
140+
fn re_macro() {
141+
// >1 regex! should work in a scope since the static is in a block
142+
assert!(regex!("foo").is_match("foo"));
143+
assert!(regex!("bar").is_match("bar"));
144+
145+
let re: &Regex = regex!("foo");
146+
ensure_static(re);
147+
148+
fn ensure_static(_re: &'static Regex) {}
149+
}
150+
151+
#[test]
152+
fn re_bytes_macro() {
153+
// >1 regex! should work in a scope since the static is in a block
154+
assert!(bytes::regex!("foo").is_match(b"foo"));
155+
assert!(bytes::regex!("bar").is_match(b"bar"));
156+
157+
let re: &bytes::Regex = bytes::regex!("foo");
158+
ensure_static(re);
159+
160+
fn ensure_static(_re: &'static bytes::Regex) {}
161+
}

0 commit comments

Comments
 (0)