Getting Started
On this page
Let's make a match. Add Ferroni to a Cargo project, run the example below, and start extracting named captures with Oniguruma syntax. Then explore iteration, replacement and the options your application needs.
Building a tokenizer? Head straight to the Scanner guide. Loading the same highlighter patterns on every start? See prepared Eager archives. Moving an existing application? See Coming from regex or onig.
Install
You'll need Rust 1.94 or newer. In an existing Cargo project, run:
cargo add ferroni
Or add it by hand:
[dependencies]
ferroni = "2"
Upgrading from 1.x? Migrating to 2.0 has before and after code for each breaking change.
Your first match
Starting fresh? Run cargo new ferroni-demo and cd ferroni-demo, then
cargo add ferroni. Paste this into src/main.rs:
use ferroni::prelude::*;
fn main() -> Result<(), RegexError> {
let regex = Regex::new(r"(?<=Date: )(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})")?;
let text = "Date: 2026-09-26";
let captures = regex.captures(text).expect("the sample date matches");
println!("input: {text}");
for name in ["year", "month", "day"] {
let value = captures.name(name).expect("the named group exists");
println!("{name}: {}", value.as_str());
}
Ok(())
}
Run it:
cargo run
You'll see:
input: Date: 2026-09-26
year: 2026
month: 09
day: 26
The look-behind (?<=Date: ) requires the label without including it in the
match. The three (?<name>...) groups make the date's parts available by name.
Compile a Regex once and reuse it for subsequent searches.
For the whole match without captures, use find; for a yes-or-no answer,
use is_match. String searches return byte offsets, ready for &str
slicing. When you need to handle a search limit explicitly, use the fallible
*_with methods in Handling errors.
Captures
Oniguruma accepts three spellings for a named group -- (?<name>...),
(?'name'...) and (?P<name>...) -- and all of them resolve through
Captures::name. Group 0 is the whole match.
use ferroni::prelude::*;
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})").unwrap();
let caps = re.captures("Released on 2026-09-05.").unwrap();
assert_eq!(caps.get(0).unwrap().as_str(), "2026-09-05");
assert_eq!(caps.name("year").unwrap().as_str(), "2026");
assert_eq!(caps.name("month").unwrap().as_str(), "09");
assert_eq!(caps.len(), 4); // whole match plus three groups
To walk every match in the input, use find_iter:
use ferroni::prelude::*;
let re = Regex::new(r"\w+@\w+\.\w+").unwrap();
let text = "write to ada@example.com or grace@example.org";
let found: Vec<&str> = re.find_iter(text).map(|m| m.as_str()).collect();
assert_eq!(found, ["ada@example.com", "grace@example.org"]);
captures_iter walks the same matches and yields a Captures for each, so
every group of every match is available without a second search:
use ferroni::prelude::*;
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
let months: Vec<&str> = re
.captures_iter("2025-01 and 2026-10")
.map(|caps| caps.name("month").unwrap().as_str())
.collect();
assert_eq!(months, ["01", "10"]);
To resume a search at a byte offset, use find_at, captures_at or
is_match_at. They search all of the text from that offset, so a look-behind,
\b or \G still sees the bytes before it. Slicing the text would hide them.
The offset must lie on a character boundary of a &str, and it must not pass
the end of the text; either case panics.
use ferroni::prelude::*;
let re = Regex::new(r"(?<=a)b").unwrap();
assert_eq!(re.find_at("ab", 1).unwrap().range(), 1..2);
assert!(re.find(&"ab"[1..]).is_none());
Replacing and splitting
replace_all substitutes every match and replace only the first. Both
return a Cow<str> that borrows the input when nothing matched. In a
replacement, $1, ${1}, $name and ${name} insert a capture group, and
$$ inserts a literal $.
use ferroni::prelude::*;
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
assert_eq!(re.replace_all("2025-01 and 2026-10", "$month/$year"), "01/2025 and 10/2026");
assert_eq!(re.replace("2025-01 and 2026-10", "${year}"), "2025 and 2026-10");
As in Oniguruma, a pattern with named groups does not capture its plain
(...) groups. So $2 expands to nothing here, and capture_group(true)
turns the plain groups back on:
use ferroni::prelude::*;
let re = Regex::new(r"(?<y>\d+)-(\d+)").unwrap();
assert_eq!(re.replace_all("2026-10", "$y/$2"), "2026/");
let re = Regex::builder(r"(?<y>\d+)-(\d+)").capture_group(true).build().unwrap();
assert_eq!(re.replace_all("2026-10", "$y/$2"), "2026/10");
A closure receives the captures of each match and returns the replacement.
NoExpand inserts its text as it is, with no $ expansion:
use ferroni::prelude::*;
let re = Regex::new(r"\d+").unwrap();
let doubled = re.replace_all("1 and 21", |caps: &Captures| {
(caps[0].parse::<u32>().unwrap() * 2).to_string()
});
assert_eq!(doubled, "2 and 42");
assert_eq!(re.replace_all("a1", NoExpand("$0")), "a$0");
split yields the pieces between the matches, and splitn stops after a
given number of pieces, leaving the rest of the text unsplit in the last one:
use ferroni::prelude::*;
let re = Regex::new(r"\s*,\s*").unwrap();
let fields: Vec<&str> = re.split("ada , grace,linus").collect();
assert_eq!(fields, ["ada", "grace", "linus"]);
let head: Vec<&str> = re.splitn("ada, grace, linus", 2).collect();
assert_eq!(head, ["ada", "grace, linus"]);
Replacing and splitting walk the matches of find_iter, so they report an
empty match right after a non-empty one, as find_iter does. For a* on
ab, the matches are a, then an empty match at 1, then one at 2. The
regex crate skips the empty match at 1, so its results differ here:
use ferroni::prelude::*;
let re = Regex::new(r"a*").unwrap();
assert_eq!(re.replace_all("ab", "-"), "--b-");
assert_eq!(re.split("ab").collect::<Vec<_>>(), ["", "", "b", ""]);
Compile-time options
Inline flags such as (?i) work as they do in Oniguruma. For the options that
have no inline spelling, or when you would rather not touch the pattern, use
the builder:
use ferroni::prelude::*;
let re = Regex::builder(r"hello")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("Hello World"));
RegexBuilder also exposes dot_matches_newline, multi_line_anchors,
extended, a raw option escape hatch for any ONIG_OPTION_* flag, and
syntax_mode for selecting a typed Syntax such as Syntax::Ruby or
Syntax::Python. All twelve Oniguruma syntax modes are included.
Compile once, search many times
Choose when Ferroni prepares optional search accelerators:
Scroll sideways for more columns.
| Mode | Preparation |
|---|---|
AccelerationMode::Auto | The default: prepare helpers on demand and promote eligible small DFAs after repeated use. |
AccelerationMode::Eager | Prepare suitable DFA and literal search accelerators before build returns. Use this for long-lived expressions when construction time matters less than search latency. |
AccelerationMode::Off | Disable the additional DFA prefilter. Ordinary compiler optimizations remain enabled. Use this for expressions searched only a few times. |
use ferroni::prelude::*;
let re = Regex::builder(r"[0-9a-f]{8}-[0-9a-f]{4}")
.acceleration(AccelerationMode::Eager)
.build()
.unwrap();
assert!(re.is_match("id 0123abcd-0042"));
Clones share the prepared data. dfa(true) selects Auto, and dfa(false)
selects Off; the last dfa or acceleration call wins. Eager preparation
retains pattern and memory limits and falls back when a DFA is unsuitable.
Hybrid fallback DFAs still compute transitions during searches, and threads
may still allocate search scratch space. Eager preparation is not a guarantee
of allocation-free searches or a fully precomputed DFA for every pattern.
DFA acceleration requires the default dfa-prefilter Cargo feature. Without
it, Eager still prepares deferred literal searchers. The
optimization details
explain when the accelerator can answer a search directly; the
limit contract explains how that
interacts with process-wide limits. The fallible *_with searches always run
the matcher.
Put Oniguruma syntax to work
Find repeated words, extract text after a label, or match a whole grapheme cluster. These examples show why you might reach for Ferroni:
use ferroni::prelude::*;
// Backreference: the same word twice in a row.
let doubled = Regex::new(r"\b(\w+) \1\b").unwrap();
assert_eq!(doubled.find("this this is repeated").unwrap().as_str(), "this this");
// Variable-length look-behind.
let after_label = Regex::new(r"(?<=version:\s*)\d+\.\d+\.\d+").unwrap();
assert_eq!(after_label.find("version: 1.3.3").unwrap().as_str(), "1.3.3");
// Unicode properties -- 914 property names from Unicode 18.0 data.
let greek = Regex::new(r"\p{Greek}+").unwrap();
assert_eq!(greek.find("alpha: αβγ").unwrap().as_str(), "αβγ");
// Grapheme clusters.
let cluster = Regex::new(r"\X").unwrap();
assert_eq!(cluster.find("é!").unwrap().as_str(), "é");
Lookbehind validation follows Oniguruma's parser, including its restrictions on nested lookbehind. For example, Oniguruma rejects the positive form below with an invalid-lookbehind syntax error, and Ferroni preserves that behavior:
use ferroni::prelude::*;
assert!(Regex::new(r"(?<=(?<!a)b)c").is_err());
The superficially similar outer negative form (?<!(?<!a)b)c is accepted.
Ferroni keeps this distinction for compatibility with the C engine.
Conditionals (?(cond)then|else), absent expressions (?~...),
subexpression calls \g<name>, callouts (?{...}), (*FAIL), and the
retry/time/stack safety limits are all supported as well.
The Scanner API
Building a tokenizer or a TextMate highlighter? Scanner searches a set of
patterns and returns the earliest match, with captures in byte or UTF-16
positions. The Scanner guide walks through the first match,
a complete tokenization loop and sharing compiled patterns across scanners.
Jump to UTF-16 positions, repeated line scanning, syntax options or pattern sharing.
Handling errors
Compilation returns Result<_, RegexError>. The error groups Oniguruma's
roughly one hundred error codes into semantic variants, so you can react to a
bad pattern differently than to an exhausted limit.
use ferroni::prelude::*;
let err = Regex::new(r"(unclosed").unwrap_err();
assert!(matches!(err, RegexError::Syntax { .. }));
println!("{err}");
RegexError::Syntax and RegexError::Encoding carry the upstream code and
message; MatchStackLimitOver, RetryLimitInMatchOver, TimeLimitOver,
SubexpCallLimitOver, and ParseDepthLimitOver report the safety limits.
Compilation errors and search outcomes are separate. Plain methods such as
find and captures return None for both no match and a stopped search.
For user-supplied text or patterns, use SearchOptions with the fallible
*_with methods: Ok(Some(...)) means a match, Ok(None) means no match,
and Err(...) reports a limit or another search error. Start with the
untrusted input guide.
Platforms and build features
Cargo builds Ferroni without a C toolchain. CI tests Linux, macOS, Windows and
WASI, and builds for browser WebAssembly. The ffi and onigmo features are
for this repository's comparison benchmarks; leave them off in applications.
Avoid --all-features when building Ferroni as a dependency, since those
benchmark features require local C source checkouts.
On wasm32-unknown-unknown, use retry limits instead of timeouts: that target
has no clock. wasm32-wasip1 supports timeouts. See
per-search limits for examples.
The low-level C API
The ported C entry points retain their original names. Use them when you need an operation the idiomatic layer does not expose. Rust ownership replaces C allocation and cleanup functions; the compatibility guide lists the intentionally omitted functions.
use ferroni::regcomp::onig_new;
use ferroni::regexec::onig_search;
use ferroni::oniguruma::*;
use ferroni::regsyntax::OnigSyntaxOniguruma;
let reg = onig_new(
b"\\d{4}-\\d{2}-\\d{2}",
ONIG_OPTION_NONE,
&ferroni::encodings::utf8::ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
).unwrap();
let input = b"Date: 2026-02-12";
let (result, _region) = onig_search(
®, input, input.len(), 0, input.len(),
Some(OnigRegion::new()), ONIG_OPTION_NONE,
);
assert_eq!(result, 6); // match starts at byte 6
Ferroni supports ASCII and UTF-8; the other 27 upstream encodings are out of scope (see ADR-003).
Where to go next
- Scanner guide — build a tokenization loop, use UTF-16 offsets and share compiled patterns.
- Untrusted Input -- per-search limits, the compile-time backtracking check, and memory safety.
- Compatibility -- what is covered, where Ferroni differs from C on purpose, and the test parity with the upstream suite.
- Coming from regex or onig -- where the defaults differ
from the
regexandonigcrates, and how to getregex-like behavior. - Migrating to 2.0 -- the breaking changes of 2.0, with before and after code for each.
- Regex Benchmarks -- the speed summary, the source projects, the method and where the full results live.
- ADR-001 -- why the port is structurally 1:1 with the C original, and what that means for contributions.
- ADR-006 -- the design of the Scanner API.
- The repository and the crate.