Cat Implementation in Zig 0.16

Zig 0.16.0 introduces a refined std.Io system that makes working with files standard streams, and I/O operations more consistent and pleasant. In this post, we will walk through how to implement a simple yet functional version of the classic Unix cat(1) command using modern Zig idioms.

When run the program with no arguments, it reads from standard input and writes the content to standard output. When given one or more file paths as arguments, it opens each file and prints its contents in sequence.

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    const stdout = std.Io.File.stdout();
    const stderr = std.Io.File.stderr();
    const args = try init.minimal.args.toSlice(allocator);

    if (args.len == 1) {
        // Pass the raw stdin file handle directly
        try catStream(init.io, std.Io.File.stdin(), stdout);
    } else {
        var i: usize = 1;
        while (i < args.len) : (i += 1) {
            const path = args[i];
            const file = std.Io.Dir.cwd().openFile(
                init.io,
                path,
                .{},
            ) catch |err| {
                const msg = try std.fmt.allocPrint(
                    allocator,
                    "cat: {s}: {s}\n",
                    .{ path, @errorName(err) },
                );
                try stderr.writeStreamingAll(init.io, msg);
                continue;
            };
            defer file.close(init.io);

            try catStream(init.io, file, stdout);
        }
    }
}

fn catStream(
    io: std.Io,
    reader: std.Io.File,
    writer: std.Io.File,
) !void {
    var buf: [128]u8 = undefined;
    while (true) {
        const n = reader.readStreaming(io, &.{&buf}) catch |err| {
            if (err == error.EndOfStream) break;
            return err;
        };
        try writer.writeStreamingAll(io, buf[0..n]);
    }
}

The program uses std.process.Init as the entry point — the modern and recommended style in Zig 0.16.0. This provides convenient access to an allocator, the I/O context and command-line arguments with minimal setup.

Memory management is handled with an ArenaAllocator, which is well-suited for short-lived command-line tools. All allocations are automatically cleaned up when the arena is deinitialized at the end.

File handling is built around std.Io.File. Standard streams (stdin, stdout, stderr) and regular files are treated uniformly through the same clean interface. Files are opened using std.Io.Dir.cwd() and properly closed with defer.

The core logic is contained in the reusable catStream() function. It reads data in small chunks using a fixed-size buffer and writes it directly to the output stream. The loop exits cleanly when the end of the input is reached, while still propagating unexpected errors.

Error reporting is user-friendly: messages are formatted with the filename and error name, then written to stderr, allowing the program to continue processing other files.

Happy coding in Zig!