Standard library · Text
text/regex
Imported as import "text/regex" as regex, its names are then regex.…. Every signature below is the one the checker infers.
Regular expressions: a small backtracking matcher, written in Rill.
What it knows: literals and the escapes \. \\ \n \t; . (anything but a newline); classes [abc], [a-z0-9], [^...], and \d \w \s with their negations \D \W \S, alone or inside a class; the anchors ^ and $; groups (...), which capture, and (?:...), which do not; |; and the counts * + ? {n} {n,} {n,m}, each greedy, or lazy with a ? after it. A match is the leftmost one, alternatives tried in the order written and counts as many as they can — Perl's answer, not POSIX's longest.
Bytes, like every string here: . is one byte, and a class of é is a class of its two bytes. Backtracking keeps the matcher small and the patterns familiar, and can be slow on a pattern built to defeat it — (a*)*b on a long run of as — as it is everywhere else it is used.
date = regex.compile("(\\d{4})-(\\d{2})-(\\d{2})")? regex.is_match(date, "on 2026-09-14 at") # => true regex.find(date, "on 2026-09-14 at") # => Some((3, 13)) regex.captures(date, "on 2026-09-14 at") # => Some(Cons(2026-09-14, Cons(2026, Cons(09, Cons(14, Nil))))) spaces = regex.compile("\\s+")? regex.replace_all(spaces, "a b c", " ") # => a b c regex.split(regex.compile(",\\s*")?, "a, b,c") # => Cons(a, Cons(b, Cons(c, Nil)))
Types
Node
Lit(byte: Int)AnyClass(set: Buf(U8))StartEndSeq(items: List(Node))Alt(branches: List(Node))Group(index: Int, inner: Node)Rep(inner: Node, least: Int, most: Int, greedy: Bool)
Re
A compiled pattern: the tree and how many groups it captures.
Re(node: Node, groups: Int)
Functions
fn is_match(r: Re, s: Str) -> Bool
r = regex.compile("^[a-z]+$")? regex.is_match(r, "hello") # => true regex.is_match(r, "Hello") # => false
fn find(r: Re, s: Str) -> Option((Int, Int))
Where the first match is: Some((start, end)), the end excluded.
r = regex.compile("\\d+")? regex.find(r, "room 101") # => Some((5, 8)) regex.find(r, "no digits") # => None
fn find_from(r: Re, s: Str, from: Int) -> Option((Int, Int))
r = regex.compile("\\d+")? regex.find_from(r, "1 and 22", 1) # => Some((6, 8))
fn find_all(r: Re, s: Str) -> List((Int, Int))
Every match, left to right, none overlapping; an empty match moves on by one byte so that the search always ends.
r = regex.compile("\\d+")? regex.find_all(r, "1, 22, 333") # => Cons((0, 1), Cons((3, 5), Cons((7, 10), Nil)))
fn captures(r: Re, s: Str) -> Option(List(Str))
The match and every group, group 0 the whole; a group that took no part is "".
r = regex.compile("(\\w+)@(\\w+)")? regex.captures(r, "mail me@host now") # => Some(Cons(me@host, Cons(me, Cons(host, Nil))))
fn captures_from(r: Re, s: Str, from: Int) -> Option(List(Str))
r = regex.compile("(\\d)")? regex.captures_from(r, "1 2", 1) # => Some(Cons(2, Cons(2, Nil)))
fn replace_all(r: Re, s: Str, with: Str) -> Str
Every match replaced by what f makes of it.
r = regex.compile("[aeiou]")? regex.replace_all(r, "banana", "_") # => b_n_n_
fn replace_by(r: Re, s: Str, f: (Str) -> Str) -> Str
r = regex.compile("\\d+")? regex.replace_by(r, "a1b22", \m -> "<" + m + ">") # => a<1>b<22>
fn split(r: Re, s: Str) -> List(Str)
The pieces between the matches.
r = regex.compile("\\s*;\\s*")? regex.split(r, "a ; b;c") # => Cons(a, Cons(b, Cons(c, Nil)))
fn compile(pattern: Str) -> Result(Re, Str)
res_map(regex.compile("a(b"), \r -> r.groups) # => Err(`a(b`: `(` is never closed at byte 1) r = regex.compile("a+")? regex.is_match(r, "caab") # => true
Tests
test_literals_classes_anchorstest_counts_and_groupstest_all_replace_splittest_bad_patterns