Comptime over runtime. Conventions over registration.
Zig already has good CLI libraries — the difference is the level they operate at. Here's the philosophy, an honest comparison, and every measured number behind the "batteries-included" claim.
Philosophy
Comptime over runtime. The command tree, the argument parser, and the routing table are all generated at build time from your files and your structs. There's no filesystem scan at startup, no reflection, no registry object built on every run — the binary that ships already knows its own shape.
Conventions over registration. A file in commands/ is a command. A folder is a group. There's no addCommand() call to forget, no builder chain to keep in sync with the file that implements it — the two can't drift apart because they're the same artifact.
Batteries with opt-out. Every package — zinput, zprogress, ztheme, terminal, vterm — works standalone, with no dependency on the framework. Use the whole thing, or take one package and leave the rest.
Stable Zig only. zcli targets Zig 0.16.0 — the current stable release, no nightly required. That's a deliberate constraint: a framework you can't build tomorrow because it tracked a moving target isn't actually batteries-included.
Comparison
zig-clap is an argument parser, and the lightest of the three: describe flags in a comptime help-text DSL, get typed results back, build the rest of the CLI yourself. If flag parsing is all you need, it's a great choice. zli is a batteries-included framework built around a runtime builder: commands are constructed with Command.init(...), wired up with addCommand, flags registered with addFlag and read by name.
zcli moves that work to the filesystem and the compiler. The directory tree is the command tree, discovered at build time with routing generated as ordinary Zig code. And the batteries extend past parsing into the whole terminal experience.
| zig-clap | zli | zcli | |
|---|---|---|---|
| Scope | argument parser | CLI framework | CLI framework |
| Commands defined by | manual dispatch | runtime builder | files on disk, discovered at build time |
| Flags are | comptime DSL → typed result | registered at runtime, read by name | struct fields, checked at compile time |
| Beyond parsing | help text | help, version, spinners | help, completions, prompts, progress, theming, config files, plugins, testing tools |
At the code level
The difference isn't abstract — it shows up in how you read an option value.
const verbose = ctx.flag("verbose", bool); // string key, looked up at runtime; // a typo compiles and returns a default
pub const Options = struct { verbose: bool = false, }; // options.verbose — a typo is a // compile error, not a silent default
Numbers
Every figure below is measured against zcli v0.19.0, built from the showcase and a minimal hello-world command. Expand a row for the exact commands used.
Hello-world binary, ReleaseSmall |
215 KB |
reproduce$ zcli init hello --description "hello world" $ cd hello && zig build -Doptimize=ReleaseSmall $ ls -la zig-out/bin/hello | |
Static musl binary, same command, x86_64-linux-musl |
195 KB |
reproduce$ zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSmall $ file zig-out/bin/hello # statically linked | |
| macOS runtime dependencies | libSystem only |
reproduce$ otool -L zig-out/bin/hello | |
ANSI escape sequences emitted with NO_COLOR=1 |
0 |
reproduce$ NO_COLOR=1 zig-out/bin/hello --help | grep -c $'\x1b' | |
| CI matrix | 3 OSes, every commit |
reproduceLinux, macOS, and Windows — unit tests + full e2e suite, including the interactive tier via a ConPTY backend on Windows. See .github/workflows/ci.yml | |
Stability
zcli targets stable Zig — no nightly required. main and the latest release are built and tested against the current stable Zig on Linux, macOS, and Windows in CI on every commit.
| zcli | Zig |
|---|---|
| main, v0.18.0 and later | 0.16.0 |
| v0.14.0 – v0.17.0 | 0.15.1 |
Each release is tagged twice: vX.Y.Z is the framework library — the tag for your build.zig.zon — and zcli-vX.Y.Z carries the prebuilt meta-CLI binaries that install.sh downloads. The two ship in lockstep, tags are immutable once cut, and CI enforces the version pin doesn't drift silently between them.