regex
nimony/lib/std/regex.nim
Regular expressions, and a lexer generator built on the same engine.
The engine is a DFA: a pattern is turned into a deterministic automaton once, and matching then costs one table step per input character with no backtracking. That is what makes the worst case linear — there is no input that makes (a+)+b take exponential time here — and it is also what bounds what the module can express: a capture must lie on every accepting path ((abc)|(xyz) is rejected, (abc|xyz) is fine). The automaton's size is not capped, and it can be exponential in the pattern's: (a|b)*a(a|b){20} needs two million states. Matching stays linear, but compiling a pattern from an untrusted source with tryRe can take unbounded time and memory.
Two ways to use it:
At runtime — build a Regex from a string and match against it:
`nim import std/regex
let pattern = re"[a-z0-9]+\s=\s[a-z0-9]+" echo matchLen("key1 = cal9", pattern) # 12 `
At compile time — lex turns a whole set of patterns into one automaton and emits it as a case statement, so the lexer in your program contains no regex engine at all:
nim var pos = 0 while pos < input.len: let start = pos lex input, pos: of r"\d+": echo "an integer ", substr(input, start, pos-1) of "else": echo "the ELSE keyword" of r"[a-zA-Z_]\w*": echo "an identifier" of r".": discard
The two share one implementation of what a pattern means (std/private/regexcore), so a pattern cannot mean one thing in a generated lexer and another at runtime.
Captures ========
(x) records where the group matched; matchLen/match/fullMatch take a var seq[Capture] and capture reads the text back out:
nim var caps: seq[Capture] = @[] if match("key=value", re"(\w+)=(\w+)", caps): echo capture("key=value", caps, 0) # key echo capture("key=value", caps, 1) # value
A DFA is a poor place to track captures, and this one is honest about it rather than clever. Two limits follow:
- A capture must lie on every accepting path, so
(abc)|(xyz)is a
compile-time error. Write (abc|xyz).
- Otherwise the match is always right, but the capture bounds are
best-effort: for a group sitting next to something of variable length ((\w+)\s*=) the engine cannot say where the group ended, and caps comes back empty. capture then returns "". Check for it during development rather than assuming; a fixed-shape pattern always works.
Where captures are essential and the pattern is awkward, split it: match the overall shape with one regex and pull the pieces out with split or plain string operations.
Syntax ======
. any character except \0 · [abc] [^abc] [a-z] character classes · x* x+ x? x{m,n} repetition · x|y alternation · (x) capture · (?:x) grouping without a capture · "abc" a literal run · \d \D \s \S \w \W classes · \A (^) start, \Z ($) end, \b \B word boundary · \1 back reference · \n \r \t \e \a \v \f \b and \123 escapes.
By default patterns are parsed with reExtended, so unescaped spaces and tabs are ignored and a pattern may be laid out for reading. Match a literal space with \ , [ ] or " ".
template re(pattern: string): RegexThe regular expression
pattern, compiled while your program is compiled.nim let assignment = re"[a-z0-9]+\s*=\s*[a-z0-9]+"patternmust be a string literal. A malformed one is a compile-time error naming what is wrong with it, and no automaton is built at run time: what reaches the binary is the finished program the matcher walks. That matters more than it sounds — the subset construction runs over a 260-letter alphabet, so building even a small regex at start-up costs far more than matching with it ever will.A
rein a loop still rebuilds its (small) literal on every iteration; bind it to aletoutside the loop, as above.For a pattern that is only known at run time — from a config file, a command line, a request — use
tryRe, which hands back the diagnostic instead of failing the build.template re(pattern: string; flags: set[RegexFlag]): Regexrewith explicit flags. Both arguments must be literals: the pattern a string literal,flagsa set constructor such as{reNoCaptures}.proc tryRe(pattern: string; dest: var Regex; err: var string; flags: set[RegexFlag]): boolCompiles a pattern that is not known until run time. On failure
errsays what is wrong with it anddestis left matching nothing.This is the form to use on a pattern that came from outside the program. An invalid pattern is then an ordinary input rather than a bug, and its diagnostic is something to show, not something to crash on.
func isEmpty(r: Regex): boolTrue for a regex that never matches — what
tryReleaves behind when it fails.func captureCount(r: Regex): int64The number of capture groups in
r.func matchLen(s: string; r: Regex; caps: var seq; start: int64): int64The length of the longest match of
rstarting exactly atstart, or-1when there is none. Capture bounds are written tocaps, which is grown as needed; its entries are absolute positions intos.func matchLen(s: string; r: Regex; start: int64): int64The length of the longest match of
rstarting exactly atstart, or-1when there is none.func match(s: string; r: Regex; start: int64): boolDoes
rmatch atstart? The match need not reach the end ofs— usefullMatchfor that, or anchor the pattern with\Z.func match(s: string; r: Regex; caps: var seq; start: int64): boolmatch, additionally reporting the capture bounds.func fullMatch(s: string; r: Regex; start: int64): boolDoes
rmatch all ofs[start..]?func fullMatch(s: string; r: Regex; caps: var seq; start: int64): boolfullMatch, additionally reporting the capture bounds.func findBounds(s: string; r: Regex; caps: var seq; start: int64): tuple[int64, int64]The bounds of the first match at or after
start, both ends inclusive, or(-1, 0)when there is none. An empty match reportslast == first - 1.func findBounds(s: string; r: Regex; start: int64): tuple[int64, int64]The bounds of the first match at or after
start, both ends inclusive, or(-1, 0)when there is none.func find(s: string; r: Regex; start: int64): int64The index of the first match at or after
start, or-1.func contains(s: string; r: Regex): boolDoes
scontain a match ofranywhere? Written forin:if pattern in line: ….func startsWith(s: string; r: Regex): boolSame as
match(s, r), spelled for readability.func capture(s: string; caps: seq; i: int64): stringThe text of capture group
i(0-based), or""when the group took no part in the match.func captureTexts(s: string; caps: seq): seqEvery capture group's text, in group order.
iterator findAll(s: string; r: Regex; start: int64): tuple[int64, int64]The bounds of every non-overlapping match, left to right, both ends inclusive. An empty match advances by one character, so the loop always terminates.
func replace(s: string; r: Regex; by: string): stringEvery non-overlapping match of
rreplaced byby, which is inserted literally — there is no$1substitution.func split(s: string; r: Regex): seqssplit at every non-empty match ofr. The separators are dropped and empty fields are kept, so the pieces and the separators together reconstructs.template lex(s: string; pos: var int64; sections: varargs[untyped]): untypedMatches the patterns of its
ofbranches againstsatpos, runs the branch that wins and leavesposjust past the match.nim var pos = 0 while pos < input.len: let start = pos lex input, pos: of r"\d+": echo "an integer ", substr(input, start, pos-1) of "else": echo "the ELSE keyword" of r"[a-zA-Z_]\w*": echo "an identifier" of r".": discardEvery pattern goes into one automaton, so a step does not get more expensive as branches are added: a hundred keywords cost what one costs.
The longest match wins; between two patterns that match the same text, the branch written first wins — which is why a keyword has to be listed before the identifier pattern that also covers it. The scan runs on past an accepting state looking for something longer and rewinds when it fails, so a pattern that gets halfway and dies costs nothing.
When nothing matches,
posdoes not move and no branch runs; anelsebranch catches that case. Give the last branch a catch-all pattern (r".") when the loop around it relies on making progress.The patterns must be string literals, and they are compiled while your program is compiled: what reaches the binary is a
casestatement over the automaton's states, with no regex engine behind it.template rematch(s: string; sections: varargs[untyped]): untypedRuns the branch whose pattern matches all of
s:nim rematch token: of r"\d+": echo "a number" of r"[a-z]+": echo "a word" of r"\w+", r"-": echo "something else word-shaped" else: echo "no idea"It is
lex's sibling for classifying a whole string rather than scanning one, and the compile-time counterpart of a chain offullMatchtests: one automaton is built for all the branches together, so the cost of a classification does not grow with the number of alternatives.Between two patterns that both match, the branch written first wins. When none matches, the
elsebranch runs — and when there is none, nothing does.