Skip to content

Ignore

zpack.Ignore is the .zpackignore engine. pack uses it internally, and it is public so a build script or asset pipeline can apply the same rules without packing anything.

var ignore = try zpack.Ignore.load(gpa, io, src_dir, ".zpackignore");
defer ignore.deinit();
if (ignore.match("art/hero.psd", false)) {
// excluded
}

For the pattern syntax itself, see .zpackignore.

pub fn load(gpa: Allocator, io: std.Io, dir: std.Io.Dir, sub_path: []const u8) !Ignore

Reads and parses an ignore file from dir. Returns Ignore.empty when the file is absent - the common case, and not an error.

The file is read with a 1 MiB limit.

pub fn parse(gpa: Allocator, source: []const u8) !Ignore

Parses ignore rules from memory, for a ruleset that does not come from a file:

var ignore = try zpack.Ignore.parse(gpa,
\\*.psd
\\/build/
\\!keep/keep.png
);
defer ignore.deinit();

The result owns a copy of source, so source may be freed immediately after.

Returns error.TooManyRules beyond 1024 rules. That cap bounds how much work matching a single path can cost.

pub fn match(self: Ignore, path: []const u8, is_dir: bool) bool

Whether path is excluded. path must be /-separated and relative to the archive root - the same form the archive stores.

is_dir matters: a rule written with a trailing / is directory-only and is skipped entirely for files.

Rules are applied in order and the last one to match decides, so a ! rule only undoes rules written above it.

ignore.match("art/hero.psd", false); // true, given `*.psd`
ignore.match("art", true); // false
pub const empty: Ignore = .{ .gpa = undefined, .rules = &.{}, .text = &.{} };

An Ignore with no rules. match always returns false. Safe to deinit, which is a no-op.

This is what load returns when no ignore file exists, so callers do not need a separate “no rules” path.

pub fn deinit(self: *Ignore) void

Frees the rules and the pattern text. A no-op on empty.

pub const Rule = struct {
/// Leading and trailing `/` already stripped.
pattern: []const u8,
/// The rule un-ignores anything it matches.
negate: bool,
/// Written with a trailing `/`, so it matches directories only.
dir_only: bool,
/// Matched against the whole path rather than the basename.
anchored: bool,
};

rules is public, so you can inspect what a file parsed to:

for (ignore.rules) |rule| {
std.debug.print("{s}{s}{s}\n", .{
if (rule.negate) "!" else "",
rule.pattern,
if (rule.dir_only) "/" else "",
});
}

Patterns borrow from the Ignore’s internal text buffer and are invalidated by deinit.

pub const max_rules: usize = 1024;

Enough for any sane ignore file, and a bound on how much work one path can cost to match. Exceeding it is error.TooManyRules.

Ignore answers one question about one path. It does not walk anything.

The pruning behaviour - an ignored directory is never descended into, so a negation cannot resurrect a file underneath it - comes from how pack uses the matcher, not from the matcher itself:

while (try walker.next(io)) |dir_entry| {
const is_dir = dir_entry.kind == .directory;
if (ignore.match(archive_path, is_dir)) continue; // never entered
if (is_dir) {
try walker.enter(io, dir_entry);
continue;
}
// ...pack the file...
}

If you use Ignore in your own walk and want the same semantics, prune the same way: check the directory before entering it, and skip entering when it matches. Filtering afterwards gives different - and slower - results.

Character classes ([a-z]) are not supported, and only one ignore file is read per archive - nested ones in subdirectories are ignored. See .zpackignore for the full comparison.