A small wrapper over std.fs.Dir.Walker

Tested only with zig version 0.12.0 on Linux Fedora 39.

zdirwalker is a small wrapper over std.fs.Dir.Walker.

From std.fs.Dir.Walker.next documentation:
“After each call to this function, and on deinit(), the memory returned from this function becomes invalid. A copy must be made in order to keep a reference to the path.”

DirWalker make that copy and store some extra information in a structure.

This module was made in an attempt to learn the zig language and is not very useful. Zig has such a solution by default and I used in build.zig module file (see setupExample()).

However, for situations with many directories/files, and which need to be traversed often, it can be useful.

Thanks @dimdin for your help and @tensorush for this guide Build system tricks.

That’s all about this module.

All the best!

UPDATE:
Tested with zig version 0.13.0-dev.242+6635360db works fine.

2 Likes

I read your Git page (looks nice btw). Can you post some examples here? I think it would help to see a few use cases.

1 Like

Thanks @AndrewCodeDev, the README is maybe a little too nice (documentation looks very similar), but I was in a good mood. :slightly_smiling_face:

To use the module it is important to know where the entry directory is, and where the walking directory is relative to the entry.

Although it seems simple in logic, it is not always easy to understand and apply, especially when we keep going up and down through directories.

Even the module is not run directly in terminal, the entry is always current working directory cwd, from where the application starts.

Here, in first example, cwd is the terminal directory.

const std = @import("std");
const print = std.debug.print;

const zdirwalker = @import("zdirwalker");
const DirWalker = zdirwalker.DirWalker;
const ArrayList = zdirwalker.ArrayList;
const Info = zdirwalker.Info;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer if (gpa.deinit() == .leak) print("{s}\n", .{"memory leak"});

    var tester = try DirWalker(ArrayList(Info)).init(allocator);
    defer tester.deinit();

    //+ `"."` is the current working directory (cwd)
    //+ usually cwd is the project directory (i.e. the build.zig directory)
    //+ in this case `<some_path>/zdirwalker`
    tester = try tester.walk(".");

    print("========================================\n", .{});
    print(
        "root_name:  {s}\n  root_path:  {s}\n",
        .{ tester.root.name, tester.root.path },
    );
    print("========================================\n", .{});

    //+ this can be a very long list, we will only print the first 5 entries
    var index: usize = 0;
    for (tester.content.items) |cont| {
        if (index < 5) {
            print(
                "cont_name:  {s}\n  cont_path:  {s}\n",
                .{ cont.name, cont.path },
            );
            print("----------------------------------------\n", .{});

            index += 1;
        } else break;
    }
}

Exemple is egs/cwddir in repo and we can run like:

zdirwalker$ zig build run-cwddir
...

For this run, entry is <some_path>/zdirwalker and walking is zdirwalker.

The same example, run like this, leads to totally different results:

zdirwalker$ cd zig-out
zdirwalker/zig-out$ ./egs/cwddir
...

Here, entry is <some_path>/zdirwalker/zig-out and walking is zig-out.

Next example is egs/exedir-twoup-onedown in repo.

The code is as in the first example, but with this change:

...
    //+ `directory` is two levels up and one down from executable directory
    //+ in this case `<some_path>/zdirwalker/src`
    var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
    var directory = try std.fs.selfExeDirPath(&buffer);

    directory = try std.fs.path.join(
        allocator,
        &[_][]const u8{ directory, "../../src" },
    );
    defer allocator.free(directory);

    tester = try tester.walk(directory);
...

For this example, entry is <some_path>/zdirwalker, the concept of entry remains, but we don’t care about it because walking is an absolute path.

The first step:

var directory = try std.fs.selfExeDirPath(&buffer);

returns an absolute path of the executable.

The second step:

directory = try std.fs.path.join(
    allocator,
    &[_][]const u8{ directory, "../../src" },
);

returns the walking absolute path.

Note that the join part to directory must be a fully qualified relative path to directory.
Mistakes can easily be made in defining these relative path.

We can run like:

zdirwalker$ zig build run-exedir-twoup-onedown
...

or

zdirwalker$ cd zig-out
zdirwalker/zig-out$ ./egs/exedir-twoup-onedown
...

The terminal output remains the same:

========================================
root_name:  src
  root_path:  <some_path>/zdirwalker/src
========================================
cont_name:  zdirwalker
  cont_path:  zdirwalker.zig
----------------------------------------

Finally, I would like to point out some aspects about build.zig, which may be of interest:

  • each step can run independently of each other or with a minimal and necessary dependency (e.g. code coverage cov need tests tst)
  • cov step now run as expected, it was a bit of a challenge for me to get it to work before there was a zig-out directory
  • fmt step now really formats zig files by default, as it was initially I found it very confusing and had to dig to understand how it worked (Hey! Format my files, don’t print this weird message.) :laughing:
  • all removes are relative to project root and zig-out directory
  • examples are taken directly from the examples directory, no need for a list of examples in the build.zig file

Getting the examples to work was by far the biggest challenge for me as some library errors were propagating in the standalone project, but @dimdin helped me out and now it works as expected.

Here are all steps:

zdirwalker$ zig build -l
install (default)            Copy build artifacts to prefix path
uninstall                    Remove build artifacts from prefix path
lib                          Build static library   (zig-out/lib)
tst                          Run tests
cov                          Generate code coverage (zig-out/cov)
doc                          Generate documentation (zig-out/doc)
fmt                          Silent formatting
rm-cache                     Remove cache           (zig-cache)
rm-out                       Remove output          (zig-out)
rm-bin                       Remove binary          (zig-out/bin)
rm-cov                       Remove code coverage   (zig-out/cov)
rm-doc                       Remove documentation   (zig-out/doc)
rm-egs                       Remove examples        (zig-out/egs)
rm-lib                       Remove library         (zig-out/lib)
run-cwddir                   Run example cwddir
run-cwddir-oneup             Run example cwddir-oneup
run-exedir                   Run example exedir
run-exedir-oneup             Run example exedir-oneup
run-exedir-twoup-onedown     Run example exedir-twoup-onedown

I’m sorry for this very long post, but I didn’t know how to make it shorter.

All the best!