A type-safe command-line argument parser for Zig. Taking inspiration from Rust clap, and TigerBeetle's flags implementation, it lets you define flags using a struct or union(enum) and parses command-line arguments into it.
- Zero runtime overhead — parsing happens at comptime where possible
- Type safety — catch errors at compile time, not runtime
- Idiomatic Zig — works with the grain of the language
- Zero external dependencies
- Multiple flag types (bool, string, int, float, enum)
- Struct-based argument definition
- Default values via struct fields
- Error handling for invalid/unknown flags
- Positional arguments support
- Subcommands via
union(enum) - Slice support (multiple values per flag)
- Two parsing patterns: repeated, comma-separated
zig fetch --save git+https://github.com/doxalabs/flags.zigconst flags = b.dependency("flags", .{});
exe.root_module.addImport("flags", flags.module("flags"));const std = @import("std");
const flags = @import("flags");
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
const args = try init.minimal.args.toSlice(allocator);
// Define flags as a struct with slice support
const Args = struct {
name: []const u8 = "world",
age: u32 = 25,
active: bool = false,
// Multiple values supported
files: []const []const u8 = &[_][]const u8{},
ports: []u16 = &[_]u16{8080},
};
const parsed = try flags.parse(allocator, args, Args);
defer flags.deinit(allocator, parsed);
std.debug.print("Hello {s}! Age: {d}, Active: {}\n", .{
parsed.name, parsed.age, parsed.active
});
}./program --name=alice --age=30 --active
# Slices accept repeated flags or comma-separated values
./program --files=a.txt --files=b.txt --files=c.txt
./program --files=a.txt,b.txt,c.txtRunning with -h or --help prints your help text.
If no help declaration is found, it prints "No help available" with a hint to declare one.
Help text is defined by declaring pub const help on your struct or union type:
const Args = struct {
verbose: bool = false,
port: u16 = 8080,
pub const help =
\\Options:
\\ --verbose Enable verbose output (default: false)
\\ --port Port to listen on (default: 8080)
;
};Git-style subcommands using union(enum):
const CLI = union(enum) {
start: struct {
host: []const u8 = "localhost",
port: u16 = 8080,
},
stop: struct {
force: bool = false,
},
pub const help =
\\ Server management CLI
\\ commands:
\\ start Start the server
\\ --host Hostname to bind to (default: localhost)
\\ --port Port to listen on (default: 8080)
\\ stop Stop the server
\\ --force Force stop (default: false)
;
};
const cli = try flags.parse(allocator, args, CLI);
switch (cli) {
.start => |s| startServer(s.host, s.port),
.stop => |s| stopServer(s.force),
}Combine top-level flags with subcommands using a struct that contains a union(enum):
const CLI = struct {
verbose: bool = false,
config: ?[]const u8 = null,
command: union(enum) {
serve: struct {
host: []const u8 = "0.0.0.0",
port: u16 = 8080,
},
migrate: struct {
dry_run: bool = false,
},
},
};
const cli = try flags.parse(allocator, args, CLI);
if (cli.verbose) std.debug.print("verbose mode\n", .{});
switch (cli.command) {
.serve => |s| startServer(s.host, s.port),
.migrate => |m| runMigration(m.dry_run),
}prog --verbose serve --port=3000
prog --config=app.toml migrate --dry_runUse the @"--" marker to separate flags from positional arguments:
const Args = struct {
verbose: bool = false,
@"--": void,
input: []const u8,
output: []const u8 = "output.txt",
};
// Usage: program --verbose input.txt output.txtNote: All flag arguments (--name=value) must appear before any positional arguments. Once the first positional value is parsed, subsequent --flag arguments are treated as positional values. Use the explicit -- separator to disambiguate:
program --verbose -- input.txt --flag-is-positional- Use struct defaults for common values
- Define help via
pub const helpdeclarations - Use unions for mutually exclusive subcommands
- Leverage enums for constrained choices
- Use optional types for truly optional flags
- Don't skip error handling
- Don't make all flags optional (defeats type safety)
- Don't use runtime string manipulation for help
The parser handles syntax; the application handles semantics. Keep these in application code:
- Date/time interpretation (
--due=tomorrow) - File I/O, encryption, network calls
- Interactive prompts and terminal I/O
- Output formatting and display
- Command aliases (
t→task) - Configuration file loading
The parser extracts typed values. What you do with them is your business.
- No short flags — only long flags (
--flag=value), except-hfor help. For brevity, use--vinstead of-v - No custom types — only built-in types and enums
- No nested slices — slices of slices not supported (
[][]T) - Equals syntax only — use
--name=valuenot--name value - Strict boolean values — only
trueandfalseare accepted (no1,0,yes,no, etc.) - No subcommands + positional args — use either subcommands or positional arguments, not both in the same struct
This library draws significant inspiration from two exceptional projects:
- TigerBeetle's flags — struct-based flag definitions and zero-cost abstractions
- Rust clap — declarative API design and derive-style patterns