How to use Zeetah, the regex library for Zig
Zig's standard library has no regular expression module. Zeetah fills that gap: a fast, dependency-free regex engine with one small API. This guide shows how to use it from your own Zig code. Every example has an Open in tester link that loads it into the online regex tester.
Add Zeetah to a Zig project
Zeetah needs Zig 0.17.0 or newer and has no other dependencies. Fetch it into your build.zig.zon:
zig fetch --save git+https://github.com/DaliVana/Zeetah.git#v0.17.0
Then import the module in build.zig:
const zeetah = b.dependency("zeetah", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zeetah", zeetah.module("zeetah"));
In your code it is @import("zeetah"). The main type is zeetah.Regex.
Find matches
Compile a pattern once, then reuse it. find returns the first match, isMatch answers yes or
no, and count counts non-overlapping matches. None of them allocate.
const std = @import("std");
const zeetah = @import("zeetah");
pub fn example(allocator: std.mem.Allocator) !void {
var re = try zeetah.Regex.compile(allocator, "\\d{3}-\\d{4}");
defer re.deinit();
// The Match borrows from the input: slice, start and end (byte offsets).
if (try re.find("call 555-1234 now")) |m| {
std.debug.print("{s} at {d}..{d}\n", .{ m.slice, m.start, m.end }); // 555-1234 at 5..13
}
const yes = try re.isMatch("call 555-1234 now"); // true
const n = try re.count("555-1234 or 555-9876"); // 2
std.debug.print("{} {d}\n", .{ yes, n });
}
. matches one byte, \w and \d
are ASCII, and positions are offsets into the UTF-8 text. For ASCII text that is the same as character positions.
To walk every match, use the iterator (or findAll for an allocated slice):
var it = re.iterator("555-1234 or 555-9876");
while (try it.next(allocator)) |m| {
std.debug.print("{s}\n", .{m.slice});
}
Capture groups
find and friends return only the whole match, which keeps them fast. Ask for groups with
captures. The returned match owns its groups, so free it with deinit. groups[0] is the
whole match, groups[1] the first group, and a group that did not participate is null.
var re = try zeetah.Regex.compile(allocator, "(?<year>\\d{4})-(\\d{2})-(\\d{2})");
defer re.deinit();
if (try re.captures(allocator, "Date: 2024-03-15")) |found| {
var m = found;
defer m.deinit(allocator);
std.debug.print("{s}\n", .{m.groups[2].?.slice}); // 03
std.debug.print("{s}\n", .{m.groupByName("year").?.slice}); // 2024
}
For groups on every match, use capturesIterator and free each match as you go:
var it = re.capturesIterator("2024-03-15 and 2025-01-02");
while (try it.next(allocator)) |found| {
var m = found;
defer m.deinit(allocator);
std.debug.print("{s}\n", .{m.groupByName("year").?.slice});
}
Replace and split
replace rewrites the first match and replaceAll every match. The replacement is a template:
$0 or $& is the whole match, $1…$N are numbered groups,
${name} is a named group and $$ is a literal dollar sign. replaceLiteral and
replaceAllLiteral insert the text as-is. The result is newly allocated.
var date = try zeetah.Regex.compile(allocator, "(\\d{4})-(\\d{2})-(\\d{2})");
defer date.deinit();
const out = try date.replaceAll(allocator, "released 2024-03-15", "$3/$2/$1");
defer allocator.free(out); // "released 15/03/2024"
split returns the pieces between matches. The pieces point into your input; free only the outer slice.
var sep = try zeetah.Regex.compile(allocator, "\\s*,\\s*");
defer sep.deinit();
const parts = try sep.split(allocator, "red, green ,blue");
defer allocator.free(parts); // "red", "green", "blue"
Flags
Pass flags to compileWithFlags, or write them inline in the pattern. Both forms are equivalent, and inline
flags can also be scoped to part of a pattern, as in (?i:hello) World.
| Field | Inline | Effect |
|---|---|---|
.case_insensitive | (?i) | ASCII case-insensitive matching |
.multiline | (?m) | ^ and $ match at every line |
.dot_all | (?s) | . also matches \n |
.extended | (?x) | whitespace is ignored and # starts a comment |
var re = try zeetah.Regex.compileWithFlags(allocator, "^item", .{
.case_insensitive = true,
.multiline = true,
});
defer re.deinit();
const n = try re.count("Item one\nitem two\nnot an item"); // 2
Compile-time regex with Pattern
When the pattern is known at compile time, zeetah.Pattern compiles it during zig build and bakes
the matcher into your binary. Its methods are static, need no allocator and cannot fail at run time; an invalid pattern
is a compile error. Use Regex when the pattern comes from user input or configuration.
const Date = zeetah.Pattern("(?<year>\\d{4})-(\\d{2})-(\\d{2})", .{});
pub fn report(input: []const u8) void {
if (Date.captures(input)) |c| {
std.debug.print("{s} (year {s})\n", .{ c.slice(), c.getName("year").?.slice });
}
std.debug.print("{d} dates\n", .{Date.count(input)});
}
Pattern options are .case_insensitive and .multiline; use inline (?s) and
(?x) for the others.
Errors and safety
Zeetah returns a small, typed error set, so you can tell a broken pattern from an unsupported one:
| Error | When | Meaning |
|---|---|---|
EmptyPattern | compile | the pattern was "" |
InvalidPattern | compile | malformed syntax, such as abc( or a{3,2} |
NotImplemented | compile | valid but unsupported, such as \p{Greek} or code points above 0xFF |
PatternTooComplex | compile | too large to build within the engine's limits |
MatchBudgetExceeded | match | a backtracking pattern hit its step budget on this input |
OutOfMemory | either | an allocation failed |
Patterns without backreferences or lookaround compile to a DFA and match in linear time, so classic catastrophic
patterns are safe: (a+)+$ runs as fast as a+$. The features that need backtracking run under a step
budget and fail with MatchBudgetExceeded instead of hanging, which makes Zeetah suitable for untrusted input.
const hit = re.isMatch(untrusted_input) catch |err| switch (err) {
error.MatchBudgetExceeded => false, // treat a pathological input as no match
else => return err,
};
Regex syntax reference
Zeetah uses Perl and PCRE style syntax. All of the following works in both Regex and Pattern.
| Syntax | Matches |
|---|---|
. | any byte except newline (any byte with (?s)) |
\d \w \s | digit, word character, whitespace (ASCII); \D \W \S negate them |
\h \v \R \N | horizontal whitespace, vertical whitespace, any line break, any byte but newline |
[abc] [^abc] [a-z] | a set, a negated set, a range |
[[:alpha:]] | POSIX classes such as alpha, digit, alnum, space, punct, xdigit |
\p{L} \P{Nd} | Unicode general categories, restricted to the Latin-1 byte range |
^ $ \A \z \Z | start and end of text (of a line with (?m)) |
\b \B | word boundary, not a word boundary |
* + ? {n} {n,} {n,m} | greedy repetition |
*? +? ?? {n,m}? | lazy repetition (as few as possible) |
*+ ++ ?+ {n,m}+ | possessive repetition (never gives back) |
a|b | alternation (leftmost-first) |
(…) (?:…) (?<name>…) | capturing, non-capturing and named groups (also (?P<name>…)) |
\1 \k<name> | backreferences |
(?=…) (?!…) | lookahead, negative lookahead |
(?<=…) (?<!…) | lookbehind, negative lookbehind (fixed width) |
(?>…) | atomic group |
(?i) (?m) (?s) (?x) (?i:…) | inline flags, for the rest of the group or scoped |
\xHH \x{HH} \0 \n \t \r | byte escapes |