Regex.zig: a native Zig regex engine in the RE2 family

2 more convenient API have been added!

re.split(haystack) creates an iterator that returns all spans of the haystack split by matched regex.

test "usage: split" {
    const gpa = testing.allocator;

    var re = try Regex.compile(gpa, "[ \\t]+", .{});
    defer re.deinit();

    const haystack = "a b \t  c";
    var iter = re.split(haystack);

    try expectEqualStrings("a", iter.next().?.bytes(haystack));
    try expectEqualStrings("b", iter.next().?.bytes(haystack));
    try expectEqualStrings("c", iter.next().?.bytes(haystack));
    try expectEqual(null, iter.next());
}

re.replace and its variations are meant to turn matched bytes into the provided replacement bytes.

The base API is

pub fn replace(re: Regex, writer: std.Io.Writer, haystack: []const u8, replacement: []const u8) Writer.Error!bool

It takes a std.Io.Writer to allow caller control over the allocation of the result. If there is a match in haystack (first match), it writes the byte slice before the match, the replacement bytes, and the byte slice after the match.

The Alloc variation is a convenience method that allocates and returns result byte slice, which also serves as an example of how to use the base API.

pub fn replaceAlloc(re: Regex, gpa: std.mem.Allocator, haystack: []const u8, replacement: []const u8) !?[]const u8

The All variation replaces all matches in haystack with replacement instead of just the first match.

pub fn replaceAll(re: Regex, writer: std.Io.Writer, haystack: []const u8, replacement: []const u8) Writer.Error!bool

Finally, replacement arg is a template - if it contains tokens such as $1 or ${name} which reference the capture groups of the matched regex, then those tokens will be expanded into the capture groups of the match. See examples below.

test "usage: replace" {
    const gpa = testing.allocator;
    var w: std.Io.Writer.Allocating = .init(gpa);
    defer w.deinit();
    const writer = &w.writer;

    {
        var re: Regex = try .compile(gpa, " ", .{});
        defer re.deinit();

        {
            // Replace with std.Io.Writer API
            const replaced_once = try re.replace(writer, "a b c d", "X");
            try testing.expect(replaced_once);
            try testing.expectEqualStrings("aXb c d", w.written());
        }
        {
            // Replace, but allocate the result in the bytes
            const mb_replaced = try re.replaceAlloc(gpa, "a b c d", "X");
            const replaced = mb_replaced orelse return error.TestUnexpectedResult;
            defer gpa.free(replaced);
            try testing.expectEqualStrings("aXb c d", replaced);
        }
        {
            // Replace all matched instances, with std.Io.Writer API
            w.clearRetainingCapacity();
            const replaced = try re.replaceAll(writer, "a b c d", "X");
            try testing.expect(replaced);
            try testing.expectEqualStrings("aXbXcXd", w.written());
        }
    {
        // Replacement text can be used as a template to to expand capture groups in matched text
        var re: Regex = try .compile(gpa, "(\\w+) (\\d+), (?<year>\\d+)", .{});
        defer re.deinit();
        w.clearRetainingCapacity();

        const replaced = try re.replace(writer, "July 17, 2026", "${year}-$1-$2");
        try testing.expect(replaced);
        try testing.expectEqualStrings("2026-July-17", w.written());
    }
}

There are more low level API in the code Regex.zig which is quite easy to read - feel free to peruse!

With this I believe most useful APIs are covered. If you’re interested in taking up this library as a dependency, and have some uncovered use cases please hit me up!

6 Likes