The regex module is a ReDoS-safe
regular-expression engine: it matches in linear time and can never hang
on a pathological pattern, so it is safe to run on patterns that came
from a user or a model. It needs no capability — it is pure computation.
The dialect is the RE2 / Rust / ripgrep subset, so if
you know ripgrep's regexes you already know these.
import regex
#Compile once, then reuse
regex.compile(pattern) returns Ok(re) or a
clear Error — it never silently mis-compiles. Compile a
pattern once and reuse the re across many matches.
match regex.compile("fn\\s+(?<name>\\w+)")
Ok(re) -> ... -- use `re` below
Error(why) -> print(`bad pattern: {why}`)
end
#The match verbs
Offsets are 0-based, half-open codepoint positions —
the same convention as str.find, str.slice and
array indexing. The one rule worth remembering is the identity that
follows from it:
str.slice(s, m.start, m.stop) == m.text
The engine is codepoint-native, so multibyte text just works: an
offset counts characters, never bytes. (The stop field is
named stop, not end, because end
is a keyword.)
regex.is_match(re, s)→bool— doesrematch anywhere ins?regex.find(re, s)→{ start, stop, text } | nil— the leftmost match (half-openstart..stop), or nil.regex.find_all(re, s)→{ { start, stop, text } }— every non-overlapping match, left to right.regex.captures(re, s)→{ groups, named } | nil— the match with its groups.groupsis an array of{ start, stop, text }records (nil where a group didn't participate). Regex group numbering is 1-based —\\1is group 1 — and arrays are 0-based, so group k isgroups[k - 1].namedmaps each(?<name>)to its group.regex.replace(re, s, repl)→string— replace every match. Inrepl,$1..$9are numbered groups,${name}is a named group,$0the whole match, and$$a literal$.regex.source(re)→string— the pattern therewas compiled from.
import regex
import str
let s = "mail: ada@example here"
match regex.compile("(?<user>\\w+)@(?<host>\\w+)")
Ok(re) -> do
print(regex.is_match(re, s))
let m = regex.find(re, s)
print(`{m.start}..{m.stop} is "{m.text}"`)
print(str.slice(s, m.start, m.stop) == m.text)
print(`before the match: "{str.slice(s, 0, m.start)}"`)
let caps = regex.captures(re, s)
print(caps.named.host.text)
print(caps.groups[0].text)
print(regex.replace(re, "a@x b@y", "${host}/${user}"))
end
Error(why) -> print(why)
end
$ scua offsets.scua
true
6..17 is "ada@example"
true
before the match: "mail: "
example
ada
x/a y/b
fs.grep is the exception, and it is a deliberate one:
its line and col are 1-based,
because they are editor coordinates — the file:line:col
that grep, ripgrep and compiler diagnostics print, and that editors jump
to. Offsets into a string are 0-based; positions in a
file are 1-based.
(Inside a function that returns a Result you can use the postfix
? operator — let re = regex.compile(pattern)?
— to unwrap the Ok or return the Error to your
caller.)
#Supported syntax
Literals and . (any codepoint except newline); character
classes [a-z], [^0-9], and the shorthands
\w \s \d \W \S \D; anchors ^ $ \b \B;
alternation a|b; groups — capturing (…),
non-capturing (?:…), named (?<name>…);
quantifiers * + ? {m} {m,} {m,n} and their lazy
*? +? ?? forms; and the inline flag (?i) for
case-insensitive matching.
#Loud failure, never silent
Backreferences and lookaround cannot be supported by
a linear-time engine, so they — and other unsupported constructs — fail
at compile with a specific message that names the problem,
rather than silently matching nothing:
regex.compile("a\\1") -- Error: backreferences ... are not supported
regex.compile("(?=x)") -- Error: lookaround ... is not supported by the linear-time engine
regex.compile("(a") -- Error: unbalanced parenthesis
This matches ripgrep's default engine, which also has no backreferences or lookaround.
#On a data-table column
The same re works inside a frame filter:
import regex
let tickets = frame({
note = ["refund request", "shipped", "CHARGEBACK dispute"],
})
match regex.compile("(?i)refund|chargeback")
Ok(re) -> print(tickets.filter(note.matches(re)))
Error(why) -> print(why)
end
-- Or the multi-column shortcut:
print(tickets.search("ship", { columns = ["note"] }))
See Search a
text column. Fixed-string column filters use
note.contains("lit") (no regex needed).
#v1 limits
\w \s \d and \b use ASCII
semantics for now (identical to Unicode on ASCII identifiers, which is
what most code search needs); \p{L} and full Unicode
character classes are a planned addition and currently fail to compile.
Multiline matching (. across newlines) is not available —
patterns are line-scoped. The match atom and every offset are still
whole codepoints.