Cat Implementation in Zig

const std = @import("std");

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

    const stdout = std.fs.File.stdout();
    const stderr = std.fs.File.stderr(); // Errors go here!
    const args = try std.process.argsAlloc(allocator);

    if (args.len == 1) {
        try catStream(std.fs.File.stdin(), stdout);
    } else {
        var i: usize = 1;
        while (i < args.len) : (i += 1) {
            const path = args[i];
            const file = std.fs.cwd().openFile(path, .{}) catch |err| {
                const msg = try std.fmt.allocPrint(
                    allocator,
                    "cat: {s}: {s}\n",
                    .{ path, @errorName(err) },
                );
                try stderr.writeAll(msg);
                continue;
            };
            defer file.close();

            try catStream(file, stdout);
        }
    }
}

fn catStream(reader: anytype, writer: anytype) !void {
    var buf: [4096]u8 = undefined;
    while (true) {
        // .read() performs a system call (e.g., read(2))
        const read_bytes = try reader.read(&buf);
        if (read_bytes == 0) break; // EOF
        try writer.writeAll(buf[0..read_bytes]);
    }
}

This Zig program implements a simplified version of the Unix cat(1) command, which concatenates and displays file contents or standard input to standard output. It begins by importing the standard library (std) and defining the main() function, which can return errors (!void). An arena allocator is set up using std.heap.ArenaAllocator initialized with the page allocator; this provides a convenient way to manage memory allocations that can all be freed at once with deinit via the defer statement, helping prevent leaks. The allocator is then derived from this arena for use in subsequent operations. Raw file handles for stdout and stderr are obtained directly using std.fs.File.stdout() and std.fs.File.stderr(), allowing efficient I/O without unnecessary abstractions. Command-line arguments are allocated and parsed using std.process.argsAlloc, stored in args.

If no arguments are provided, the program treats it as reading from standard input and calls catStream() to copy from stdin to stdout. Otherwise, it iterates over each provided file path starting from index 1. For each path, it attempts to open the file in the current working directory with default read-only flags using std.fs.cwd().openFile. If opening fails, it catches the error, formats an error message using std.fmt.allocPrint (allocating the string via the arena allocator), and writes it to stderr before continuing to the next file. Successful file opens are deferred for closure, and the file’s content is streamed to stdout via catStream().

The catStream() function is a generic streamer that takes any types for reader and writer as long as they support .read() and .writeAll() methods, making it flexible for use with std.fs.File handles. It uses a fixed-size buffer of 4096 bytes to read data in chunks, looping until end-of-file (when read_bytes == 0). Each read chunk is written directly to the writer, ensuring efficient, low-level I/O with system calls like read(2). This design promotes error handling, memory safety, and performance, showcasing Zig’s emphasis on explicit control over resources while mimicking classic command-line tool behavior.

Save the code as zcat.zig and try it:

$ zig version
0.15.2
$ zig run zcat.zig
123
123
321
321 

Happy coding in Zig!