Comptime CLI argument parser for Zig 0.16+.
Type-safe options generated at compile time — no runtime allocations for the result struct.
- Comptime result types — parsed struct fields match declared types exactly
- Long and short options —
--port 8080or-p 8080 - Equals form —
--port=8080 - Flags —
--verbosesets bool to true - Enum parsing —
--mode fastwith compile-time variant validation - Optional types —
?[]const u8defaults to null - Environment variable fallback —
env: "MY_PORT"checks env before defaulting - Subcommands —
myapp beacon,myapp validatorwith per-command option sets - RC config resolver hook — plug in YAML/JSON/TOML config via a simple callback
- Option groups — organize options into named sections for help readability
- "Did you mean?" — Levenshtein-based typo correction for unknown options/commands
- Auto-generated help —
--help/-hprints formatted usage at comptime - Version flag —
--version/-V
Flags are presence-only. If you need explicit true / false parsing, use cli.option(bool, ...) instead of cli.flag(...).
Positional arguments are currently rejected unless used as the selected subcommand.
const std = @import("std");
const cli = @import("zig-cli");
const spec = .{
.name = "my-app",
.description = "My application",
.version = "0.1.0",
.options = .{
.port = cli.option(u16, .{
.long = "port",
.short = 'p',
.description = "Listen port",
.env = "MY_APP_PORT",
}, 8080),
.data_dir = cli.option([]const u8, .{
.long = "data-dir",
.short = 'd',
.description = "Data directory",
}, "./data"),
.network = cli.option(enum { mainnet, sepolia, holesky }, .{
.long = "network",
.description = "Target network",
}, .mainnet),
.verbose = cli.flag(.{
.long = "verbose",
.short = 'v',
.description = "Enable verbose output",
}),
},
};
pub fn main(init: std.process.Init.Minimal) !void {
var args_iter: std.process.Args.Iterator = .init(init.args);
const args = cli.parse(spec, &args_iter, std.heap.page_allocator) catch |err| switch (err) {
error.HelpRequested, error.VersionRequested => std.process.exit(0),
else => std.process.exit(1),
};
std.debug.print("port={d} dir={s} network={s} verbose={}\n", .{
args.port, args.data_dir, @tagName(args.network), args.verbose,
});
}const std = @import("std");
const cli = @import("zig-cli");
const app_spec = cli.app(.{
.name = "lodestar-z",
.description = "Ethereum consensus client",
.version = "0.1.0",
.commands = .{
.beacon = cli.command(.{
.description = "Run beacon node",
.options = .{
.api_port = cli.option(u16, .{
.long = "api-port",
.description = "REST API port",
.group = "api",
}, 5052),
.metrics = cli.flag(.{
.long = "metrics",
.description = "Enable metrics",
.group = "metrics",
}),
},
}),
.validator = cli.command(.{
.description = "Run validator client",
.options = .{
.beacon_url = cli.option([]const u8, .{
.long = "beacon-url",
.description = "Beacon node URL",
}, "http://localhost:5052"),
},
}),
},
.global_options = .{
.network = cli.option(enum { mainnet, sepolia, holesky }, .{
.long = "network",
.short = 'n',
.description = "Target network",
}, .mainnet),
},
});
pub fn main(init: std.process.Init.Minimal) !void {
const allocator = std.heap.page_allocator;
var args_iter: std.process.Args.Iterator = .init(init.args);
// parseApp returns a tagged union:
const result = cli.parseApp(app_spec, &args_iter, allocator) catch |err| switch (err) {
error.HelpRequested, error.VersionRequested => std.process.exit(0),
else => std.process.exit(1),
};
switch (result) {
.beacon => |opts| {
// opts.api_port, opts.metrics, opts.network (global)
},
.validator => |opts| {
// opts.beacon_url, opts.network (global)
},
}
}Global app options may appear before or after the subcommand, for example:
lodestar-z --network holesky beacon
lodestar-z beacon --network holesky
// Resolution order: CLI args > env vars > resolver > defaults
const args = cli.parseWithResolver(spec, &args_iter, allocator, struct {
fn resolve(name: []const u8) ?[]const u8 {
// Look up in YAML/JSON/TOML config — return value or null
return my_config.get(name);
}
}.resolve) catch |err| { ... };
// Also works with subcommands:
const result = cli.parseAppWithResolver(app_spec, &args_iter, allocator, resolver);.options = .{
.api_port = cli.option(u16, .{
.long = "api-port",
.description = "REST API port",
.group = "api", // ← grouped under "Api Options:"
}, 5052),
.metrics = cli.flag(.{
.long = "metrics",
.description = "Enable metrics",
.group = "metrics", // ← grouped under "Metrics Options:"
}),
.data_dir = cli.option([]const u8, .{
.long = "data-dir",
.description = "Data directory",
// no group → appears under "OPTIONS:"
}, "./data"),
},$ lodestar-z --help
lodestar-z v0.1.0 — Ethereum consensus client
USAGE:
lodestar-z <COMMAND> [OPTIONS]
COMMANDS:
beacon Run beacon node
validator Run validator client
GLOBAL OPTIONS:
-n, --network <VALUE> Target network [mainnet|sepolia|holesky] (default: mainnet)
Use 'lodestar-z <COMMAND> --help' for more information on a command.
$ lodestar-z beacon --help
lodestar-z beacon — Run beacon node
USAGE:
lodestar-z beacon [OPTIONS]
Api Options:
--api-port <NUM> REST API port (default: 5052)
Metrics Options:
--metrics Enable metrics
Global Options:
-n, --network <VALUE> Target network [mainnet|sepolia|holesky] (default: mainnet)
-h, --help Show this help
-V, --version Show version
- Zig 0.16.0+
MIT