Zeetah Regex library for Zig
GitHub

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.

Written for Zeetah 0.17.0 and Zig 0.17.0 · Updated 18 October 2026

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 });
}
Offsets are bytes. Zeetah is byte-oriented: . 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});
}

Open in tester

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});
}

Open in tester

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"

Open in tester

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"

Open in tester

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.

FieldInlineEffect
.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

Open in tester

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:

ErrorWhenMeaning
EmptyPatterncompilethe pattern was ""
InvalidPatterncompilemalformed syntax, such as abc( or a{3,2}
NotImplementedcompilevalid but unsupported, such as \p{Greek} or code points above 0xFF
PatternTooComplexcompiletoo large to build within the engine's limits
MatchBudgetExceededmatcha backtracking pattern hit its step budget on this input
OutOfMemoryeitheran 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,
};

Open (a+)+$ in tester

Regex syntax reference

Zeetah uses Perl and PCRE style syntax. All of the following works in both Regex and Pattern.

SyntaxMatches
.any byte except newline (any byte with (?s))
\d \w \sdigit, word character, whitespace (ASCII); \D \W \S negate them
\h \v \R \Nhorizontal 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 \Zstart and end of text (of a line with (?m))
\b \Bword 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|balternation (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 \rbyte escapes