Building zcat - A 179KB Cat Replacement in Zig
Building zcat
cat is the most aliased command in every developer's shell. bat, ccat, exa — everyone has a replacement. But they're all fat. bat is 15MB. It ships a syntax highlighter, a pager, and a Git integration you never asked for.
I wanted cat that fits in a kernel module.
What it does
zcat is a drop-in cat replacement. Reads files, writes to stdout. Same flags you already know: -n, -b, -s, -E, -T. Plus one you don't: --json.
That last flag is the point. AI coding agents read files by shelling out to cat. The raw text dump wastes tokens — blank lines, whitespace, repeated file paths. zcat --json gives them structured output instead:
{"files":[{"path":"main.rs","size":2048,"lines":[{"n":1,"text":"use std::io;"}]}]}
Same content. Fewer tokens. The agent parses JSON once instead of re-reading the file three times.
Why Zig
I needed three things: a small binary, fast I/O, and no dependencies.
Zig's std.io gives you streaming readers and writers with mandatory buffering. No std.io is a compile error. The File.Reader and File.Writer types handle buffering internally — you don't touch a buffer unless you want to.
For file reads, Zig's std.io.File.Reader uses positional reads (pread) by default. Falls back to streaming (read) for pipes and sockets. No configuration needed.
The binary compiles to 179KB with ReleaseSmall. No libc. No dynamic linking. Copy it anywhere.
How it works
Three files. Args.zig parses flags — no external parser, no getopt. Combined short flags work: -nbsET is valid. Cat.zig does the actual work: read, transform, write.
The read loop uses readVec with EndOfStream as the termination signal. Zig 0.16 doesn't return 0 at EOF — it returns an error. You catch it and break. Took me an hour to figure out.
Line processing is a single pass. Each line goes through a chain: squeeze blank → number → show tabs → show ends. No allocations per line. The --json mode escapes quotes, backslashes, and tabs inline — one byte at a time, no intermediate strings.
Error handling prints human-readable messages to stderr: zcat: no such file or directory, zcat: permission denied, zcat: is a directory. Exit code 1 on any error.
Performance
Tested on a 5.4MB JSON file and a 20K-line Python file:
| Operation | Time | |-----------|------| | Basic read (5.4MB) | 23ms | | All flags (5.4MB) | 24ms | | JSON output (5.4MB) | 3.3s | | All flags (20K lines) | 1.1s |
The JSON mode is slower because it serializes every line with escaping. The plain read path is fast — buffered pread on macOS, close to cat speed.
What's next
Published as v0.1.0 on GitHub. CI builds for Linux, macOS, and Windows. Tag a version, get a release with .tar.gz and .zip artifacts. Plans: -c (byte count), -v (visible non-printing), and maybe a --diff mode that only shows changed lines.
179KB binary. Zero dependencies. Pure Zig 0.16. MIT licensed.