regex

tape
import regex from "regex";

regex finds patterns in text. Compile a pattern once into a Regex, then search with it as often as you like:

tape
let re: regex.Regex = regex.compile(r"""(?<year>\d{4})-(\d{2})-(\d{2})""") or {
    io.println("bad pattern: {err}");
    return 1;
};
let text: []const u8 = "released 2026-10-13";
let c: regex.Captures = re.captures(text) or { return 1; };
io.println("year {c.named("year")!.of(text)}, month {c.get(2)!.of(text)}");

Write patterns as raw strings, r"""...""": a backslash and a brace are then just characters, as the pattern needs them (in an ordinary string, \d is an error and {4} starts an interpolation).

Matching takes time proportional to the text times the pattern, whatever both are: there are no patterns that make a search run away, so patterns and text from outside your program are safe to match. The price is that backreferences (\1) and lookaround ((?=...), (?<=...)) aren’t supported; they’re compile errors that say so. Everything else of the usual syntax is there.

Text is UTF-8: . and a class match one character (a code point), and every position the module gives you is a byte offset into the text, never inside a character. \d, \w, \s, \b and (?i) work on ASCII.

The module is pure: it only computes.

Syntax

PatternMatches
xthe character x
\xx itself, for any punctuation: \. \* \( \\ …
\t \n \r \f \v \atab, newline, carriage return, form feed, vertical tab, bell
\xHH, \x{263A}the character with that code
.any character but a newline (any at all with (?s))
[abc] [a-z] [^0-9]one character in (or not in) the set; \d \w \s work inside
\d \w \sa digit, a word character ([0-9A-Za-z_]), white space
\D \W \Sanything but those
^ $the start and the end of the text (of a line with (?m))
\A \zthe start and the end of the text, always
\b \Ba word boundary, and not one
(re)a group that captures
(?<name>re)a named group ((?P<name>re) works too)
(?:re)a group that doesn’t capture
a|ba, or else b
re* re+ re?0 or more, 1 or more, 0 or 1 (as many as possible)
re{n} re{n,} re{n,m}exactly n, at least n, n to m (up to 1000)
re*? re+? re?? re{n,m}?the same, as few as possible
(?i) (?m) (?s) (?U)flags to the end of the group: ignore case, multi-line, . matches a newline, swap greedy and lazy
(?i:re)flags for re only; (?-i) turns one off

When several matches are possible, you get the one a step-by-step matcher would find first: the leftmost, and from there the first alternative and the greedy (or lazy) repeat you wrote. a|ab finds a in ab.

Types

Regex

A compiled pattern. It frees its memory when it goes out of scope, and is copied whole on assignment. Searching doesn’t change it, so one Regex can be used by many searches, from many threads.

Span

tape
pub struct Span {
    start: i64;    // the first byte of the match
    end: i64;      // just past its last byte
}

Where a match (or a group) is. s.len() is its length in bytes and s.of(text) its text: text[s.start..s.end].

Captures

The groups of one match: c.get(i) is group i (0 is the whole match), c.named(name) a named group, both null when the group didn’t take part in the match. c.len() counts the groups with group 0. get traps for a group number the pattern doesn’t have.

RegexError

VariantValueDescriptionWhen
Syntax1invalid regular expressiona pattern that isn’t UTF-8
UnclosedGroup2missing ’)’a(b
UnopenedGroup3unmatched ’)’a)b
UnclosedClass4missing ’]’[ab
BadRange5invalid class range[z-a]
BadEscape6invalid escape\q, \x4
BadRepeat7repeat of nothing, or a repeat repeated*a, a**, a{3,2}
RepeatTooBig8repeat count above MAX_REPEATa{1001}
BadGroupName9invalid or duplicate group name(?<1x>a), two groups named x
BadFlag10unknown flag(?q)
Unsupported11not supported by a linear-time engine\1, (?=a), (?<!a), (?>a)
TooBig12pattern compiles to more than MAX_PROGRAM instructions(a{1000}){1000}

Constants

ConstantValueIs
MAX_REPEAT1000the largest count in {n,m}
MAX_PROGRAM100000instructions a pattern may compile to
MAX_GROUPS1000capturing groups in a pattern

Compiling

compile

tape
pub fn compile(pattern: []const u8) -> Regex or RegexError

The pattern compiled. Compile once and keep the Regex; compiling costs more than a search.

error_at

tape
pub fn error_at(pattern: []const u8) -> ?i64

Where compile finds the pattern wrong, as a byte offset into it, or null when it compiles. For pointing at the mistake in a message:

tape
let pat: []const u8 = "ab(cd";
let r: regex.Regex = regex.compile(pat) or {
    io.println("{err} at byte {regex.error_at(pat)!}");   // missing ')' at byte 2
    return 1;
};

escape

tape
pub fn escape(text: []const u8) -> string

text with every special character escaped: a pattern that matches exactly text. regex.escape("1+1") is 1\+1.

pattern, group_count, group_index

tape
pub fn Regex.pattern(*const self) -> []const u8
pub fn Regex.group_count(*const self) -> i64
pub fn Regex.group_index(*const self, name: []const u8) -> ?i64

The pattern it was compiled from; its capturing groups (not counting 0); the number of a named group.

Searching

is_match

tape
pub fn Regex.is_match(*const self, text: []const u8) -> bool

Whether the pattern matches anywhere in text. The fastest search: it stops at the first match it sees.

find, find_at

tape
pub fn Regex.find(*const self, text: []const u8) -> ?Span
pub fn Regex.find_at(*const self, text: []const u8, at: i64) -> ?Span

The first match, or null. find_at starts the search at byte at but sees the whole text, so ^ and \b look at what comes before at. It traps for at outside 0..text.len or inside a character.

tape
let re: regex.Regex = regex.compile(r"""\d+""") or { return 1; };
let m: regex.Span = re.find("order 66 now") or { return 1; };   // 6..8

captures, captures_at

tape
pub fn Regex.captures(*const self, text: []const u8) -> ?Captures
pub fn Regex.captures_at(*const self, text: []const u8, at: i64) -> ?Captures

The first match with its groups, or null. A group inside a repeat holds its last repetition: (a|b)+ over abab gives group 1 at 3..4.

matches

tape
pub fn Regex.matches(*const self, text: []const u8) -> MatchIter

Every match, left to right, none overlapping:

tape
let words: regex.Regex = regex.compile(r"""\w+""") or { return 1; };
for (m in words.matches("one two  three")) {
    io.println(m.of("one two  three"));
}

A pattern that can match nothing finds the empty spots too, one per character position, but never one right after the previous match: a* over baaa finds 0..0 and 1..4. The iterator borrows the Regex and the text: don’t keep it past either.

split

tape
pub fn Regex.split(*const self, text: []const u8) -> SplitIter

The text between the matches, including what comes before the first and after the last: , splits a,,b into a, empty, b. An empty match at the very start or end splits nothing, so x* splits abc into a, b, c.

tape
let sep: regex.Regex = regex.compile(r"""\s*[,;]\s*""") or { return 1; };
for (part in sep.split("a , b;c")) { io.println(part); }   // a, b, c

Replacing

replace, replace_all

tape
pub fn Regex.replace(*const self, text: []const u8, with: []const u8) -> string
pub fn Regex.replace_all(*const self, text: []const u8, with: []const u8) -> string

A new string: text with the first match (or every match) replaced. In with, $1 is group 1’s text ($0 the whole match), ${name} a named group’s, ${12} group 12, and $$ a dollar sign. A group that didn’t match is empty.

tape
let swap: regex.Regex = regex.compile(r"""(\w+)@(\w+)""") or { return 1; };
let s: string = swap.replace_all("ann@x, bob@y", "$2:$1");   // "x:ann, y:bob"

Any other $ traps (a $5 with no group 5, $x, an unclosed ${): write $$ for a dollar sign. Note that ${...} in an ordinary string is an interpolation; write "$\{name\}", or use a raw string.

Last modified: