Skip to content

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:

ModePreparation
AccelerationMode::AutoThe default: prepare helpers on demand and promote eligible small DFAs after repeated use.
AccelerationMode::EagerPrepare suitable DFA and literal search accelerators before build returns. Use this for long-lived expressions when construction time matters less than search latency.
AccelerationMode::OffDisable 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(
    &reg, 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 regex and onig crates, and how to get regex-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.