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
All offsets are 1-based codepoint positions (the
same convention as fs.grep's col), and the
engine is codepoint-native, so multibyte text just works. (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 a 1-based list (group k → its{start, stop, text}, or nil if it didn't participate);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.
match regex.compile("(?<user>\\w+)@(?<host>\\w+)")
Ok(re) -> do
print(regex.is_match(re, "a@example")) -- true
let m = regex.find(re, "mail: a@example here") -- { start=7, stop=16, text="a@example" }
let caps = regex.captures(re, "a@example")
if caps != nil then print(caps.named.host.text) end -- "example"
print(regex.replace(re, "a@x b@y", "${host}/${user}")) -- "x/a y/b"
end
Error(why) -> print(why)
end
(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.