Step-by-Step Guide: Build CLI Tools with Zig 0.12 and Clap 4.0 for Cross-Platform Support
Zig is a modern, low-level programming language designed for robustness, optimality, and clarity. Combined with Clap 4.0, a lightweight, type-safe argument parsing library, you can build fast, cross-platform CLI tools that run seamlessly on Windows, macOS, and Linux. This guide walks you through every step, from project setup to cross-compilation.
Prerequisites
Before starting, ensure you have:
- Zig 0.12.0 or later installed (verify with
zig version) - A terminal (PowerShell, Terminal.app, or Linux shell)
- Basic familiarity with command-line workflows
Step 1: Initialize Your Zig Project
Create a new directory for your CLI tool and initialize a Zig executable project:
mkdir my-cli && cd my-cli zig init-exe
This generates a standard project structure with src/main.zig (your entry point) and build.zig (build configuration).
Step 2: Add Clap 4.0 as a Dependency
Clap 4.0 is a zero-dependency argument parser for Zig. Add it to your project using Zig's package manager:
zig fetch --save git+https://github.com/Hejsil/clap.git#v4.0.0
Next, update build.zig to include Clap in your build. Modify the executable configuration to import the Clap module:
const std = @import("std"); const clap = @import("clap"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "my-cli", .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, }); const clap_dep = b.dependency("clap", .{ .target = target, .optimize = optimize, }); exe.root_module.addImport("clap", clap_dep.module("clap")); b.installArtifact(exe); }
Step 3: Write Your First CLI Logic
Open src/main.zig and replace the default content with code to parse arguments using Clap. We'll build a simple CLI that accepts a --name flag, a --verbose toggle, and a positional command argument:
const std = @import("std"); const clap = @import("clap"); const debug = std.debug; const io = std.io; const process = std.process; pub fn main() !void { const allocator = std.heap.page_allocator; const params = clap.parseParamsComptime( \\-h, --help Display this help message \\-v, --verbose Enable verbose output \\-n, --name Your name \\ Command to run (greet, goodbye) ); var diag = clap.Diagnostic{}; var res = clap.parse(clap.Help, ΒΆms, clap.parsers.default, &diag) catch |err| { diag.report(io.getStdErr().writer(), err) catch {}; process.exit(1); }; defer res.deinit(); if (res.args.help) { const stdout = io.getStdOut().writer(); try clap.usage(stdout, clap.Help, ΒΆms); try stdout.writeAll("\nExamples:\n my-cli greet --name Alice\n my-cli --verbose goodbye\n"); process.exit(0); } const verbose = res.args.verbose; const name = res.args.name orelse "World"; const command = res.positionals[0] orelse { debug.print("Error: No command provided. Use --help for usage.\n", .{}); process.exit(1); } if (std.mem.eql(u8, command, "greet")) { if (verbose) debug.print("Running greet command...\n", .{}); debug.print("Hello, {s}!\n", .{name}); } else if (std.mem.eql(u8, command, "goodbye")) { if (verbose) debug.print("Running goodbye command...\n", .{}); debug.print("Goodbye, {s}!\n", .{name}); } else { debug.print("Error: Unknown command '{s}'. Use --help for usage.\n", .{command}); process.exit(1); } }
Test the code locally by building and running:
zig build run -- --name Alice greet
You should see output: Hello, Alice!. Add --verbose to see the extra log line.
Step 4: Add Cross-Platform Compatibility
Zig handles most cross-platform differences automatically, but you may need to adjust OS-specific logic. For example, if your CLI interacts with file paths, use Zig's std.fs.path module to handle separator differences (forward slashes on Unix, backslashes on Windows). Avoid hardcoding OS-specific paths or system calls unless necessary.
To verify compatibility, test your CLI on all target platforms if possible, or use Zig's built-in cross-compilation to generate binaries for each OS.
Step 5: Cross-Compile for Target Platforms
Zig's standout feature is seamless cross-compilation without needing separate toolchains. Use the -Dtarget flag with zig build to generate binaries for Windows, macOS, and Linux, for both x86_64 and ARM64 architectures:
# Build for Windows x86_64 zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSmall # Build for macOS ARM64 (Apple Silicon) zig build -Dtarget=aarch64-macos -Doptimize=ReleaseSmall # Build for Linux x86_64 (static linking with musl) zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSmall # Build for Linux ARM64 zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSmall
The compiled binaries will be in the zig-out/bin directory. For Windows, the output will be my-cli.exe; for Unix-based systems, it will be my-cli (no extension).
Step 6: Test and Distribute
Test each compiled binary on its target platform to ensure correct behavior. For Windows, you can test via Wine if you don't have a Windows machine, but native testing is preferred. Once verified, distribute the binaries directly, or package them (e.g., using Inno Setup for Windows, DMG for macOS, or DEB/RPM for Linux).
Conclusion
With Zig 0.12 and Clap 4.0, building cross-platform CLI tools is straightforward, thanks to Zig's native cross-compilation and Clap's simple, type-safe argument parsing. You can extend this example by adding more commands, file I/O, or network requests, all while maintaining compatibility across all major operating systems. Check the Zig documentation and Clap GitHub repository for advanced features.
Top comments (0)