Skip to content

Declarative Structs

Instead of manually adding options one by one, args.zig allows you to define your configuration as a standard Zig struct and parse arguments directly into it using parseInto (or its alias derive).

TIP

For the simplest syntax when parsing process args, use parseProcessInto:

zig
var parsed = try args.parseProcessInto(allocator, Config, .{ .name = "myapp" }, init);

Or use parseInto with null for the same result:

zig
var parsed = try args.parseInto(allocator, Config, .{ .name = "myapp" }, null, init);

Use Cases

This approach is ideal for:

  1. Configuration-Driven CLIs - Define your app's entire config as a struct
  2. Rapid Prototyping - Quickly define CLI from a single struct definition
  3. Type-Safe Argument Handling - No manual type conversion or string lookups
  4. Self-Documenting Code - The struct definition shows exactly what CLI options exist
  5. Compile-Time Validation - Errors caught at build time, not runtime

How it Works

The library inspects your struct fields at compile-time and generates the corresponding ArgSpec list.

NOTE

Field names are automatically converted to kebab-case (e.g., dry_run becomes --dry-run). Types are mapped to the appropriate argument type at compile-time.

  • Field Names: Converted to kebab-case (e.g., dry_run becomes --dry-run).
  • Types: Mapped to argument types:
    • bool → Flag (.action = .store_true)
    • ?[]const u8 → Optional String Option
    • i32, i64 → Integer Option
    • f32, f64 → Float Option
    • ?T → Optional (not required)
    • T (non-bool, no default) → Required Option
    • T = value (has default) → Optional with default value applied automatically

Supported Field Types

Zig TypeCLI BehaviorExample Usage
boolFlag, defaults to false--verbose
?boolOptional flag--debug
[]const u8Required string--name value
[]const u8 = "default"Optional string with default--host localhost
?[]const u8Optional string--output file.txt
i32, i64Required integer--count 42
i32 = 10Optional integer with default--port 8080
?i32, ?i64Optional integer--port 8080
u32, u64Required unsigned integer--port 8080
u32 = 8080Optional unsigned integer with default--port 8080
f32, f64Required float--rate 0.5
f64 = 30.0Optional float with default--timeout 30.0
?f64Optional float--timeout 30.5
enumRequired enum (choices derived)--log-level info
enum = .infoOptional enum with default--log-level info

Basic Example

zig
const std = @import("std");
const args = @import("args");

// Define your configuration struct
const Config = struct {
    // Flags (bool)
    verbose: bool,
    dry_run: bool,
    
    // Optional string options
    output: ?[]const u8,
    config_file: ?[]const u8,
    
    // Optional numeric options
    timeout: ?f64,
    port: ?i32,
    
    // Required options (no default value)
    count: i32,

    // Optional with defaults (not required, applied automatically)
    host: []const u8 = "localhost",
    level: u32 = 8080,
    rate: f64 = 0.5,
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.arena.allocator();

    var parsed = try args.parseInto(allocator, Config, .{
        .name = "myapp",
        .description = "Struct-based parsing demo",
    }, null, init);
    defer parsed.deinit();

    const cfg = parsed.options;

    std.debug.print("Verbose: {}\n", .{cfg.verbose});
    std.debug.print("Dry Run: {}\n", .{cfg.dry_run});
    std.debug.print("Count: {d}\n", .{cfg.count});
    std.debug.print("Host: {s}\n", .{cfg.host});
    std.debug.print("Port: {d}\n", .{cfg.level});
    std.debug.print("Rate: {d}\n", .{cfg.rate});
    
    if (cfg.output) |out| {
        std.debug.print("Output: {s}\n", .{out});
    }
    
    if (cfg.port) |p| {
        std.debug.print("Port: {d}\n", .{p});
    }
}

Running

bash
$ myapp --count 42 --verbose
Verbose: true
Dry Run: false
Count: 42
Host: localhost
Port: 8080
Rate: 0.5

$ myapp --count 10 --output result.txt --dry-run --port 8080 --host example.com
Verbose: false
Dry Run: true
Count: 10
Host: example.com
Port: 8080
Rate: 0.5
Output: result.txt
Port: 8080

Limitations and How to Handle Complex Cases

WARNING

The parseInto approach is designed for simple to moderately complex CLIs. For advanced use cases (multi-value options, choices, subcommands), combine with the ArgumentParser API.

Limitation 1: No Multi-Value Options (Append/FIFO)

Struct-based parsing doesn't support repeatable options (like -I path1 -I path2). For this, use the traditional API with addAppend, which stores values in FIFO order (first-in, first-out):

zig
// Instead of parseInto, use ArgumentParser directly:
var parser = try args.createParser(allocator, "myapp");
defer parser.deinit();

try parser.addAppend("include", .{ .short = 'I', .help = "Include paths" });

var result = try parser.parseProcess(init);
defer result.deinit();

// Access multiple values in FIFO order
const includes = result.getArray("include"); // Returns slice of values
// -I foo -I bar -I baz → ["foo", "bar", "baz"]

Or use the hybrid approach to combine struct fields with append options:

Limitation 2: No Choices or Validation

Struct fields don't support choices or custom validators. Use the traditional API:

zig
var parser = try args.createParser(allocator, "myapp");
defer parser.deinit();

try parser.addOption("level", .{
    .choices = &[_][]const u8{ "debug", "info", "warn", "error" },
});

Limitation 3: No Subcommands

For subcommands, use the traditional ArgumentParser API:

zig
try parser.addSubcommand(.{
    .name = "build",
    .help = "Build the project",
    .args = &[_]args.ArgSpec{
        .{ .name = "target", .positional = true },
    },
});

Hybrid Approach: Combining Both Methods

TIP

You can use the derived specs as a starting point and add more options manually:

zig
const Config = struct {
    verbose: bool,
    count: i32,
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.arena.allocator();
    var parser = try args.createParser(allocator, "myapp");
    defer parser.deinit();
    
    // Add derived options from struct
    const specs = args.deriveOptions(Config);
    for (specs) |spec| {
        try parser.addArg(spec);
    }
    
    // Add additional options not in the struct
    try parser.addOption("level", .{
        .choices = &[_][]const u8{ "debug", "info", "warn", "error" },
    });
    try parser.addAppend("include", .{ .short = 'I' });
    
    var result = try parser.parseProcess(init);
    defer result.deinit();
    
    // Access values manually
    const verbose = result.getBool("verbose") orelse false;
    const count = result.getInt("count") orelse 0;
    const level = result.getString("level");
}

Using the derive Alias

For a more concise API, use the derive alias:

zig
// These are equivalent:
var parsed = try args.parseInto(allocator, Config, options, null, init);
var parsed = try args.derive(allocator, Config, options, null, init);

// Or use parseProcessInto / deriveProcess for shorter syntax:
var parsed = try args.parseProcessInto(allocator, Config, options, init);
var parsed = try args.deriveProcess(allocator, Config, options, init);

Using Global Configuration

parseInto respects the global configuration. You can centralize app metadata:

zig
// Set once at application start
args.configure(.{
    .app_name = "myapp",
    .app_version = "1.0.0",
    .app_description = "My CLI tool",
});

// parseInto will use these defaults
var parsed = try args.parseInto(allocator, Config, .{
    .name = "", // Uses app_name from global config
}, null, init);
defer parsed.deinit();

Advanced: Accessing Raw Parse Result

The returned ParseIntoResult contains both the typed options and the raw parse result:

zig
var parsed = try args.parseInto(allocator, Config, options, null, init);
defer parsed.deinit();

// Typed access (from struct)
const cfg = parsed.options;

// Raw access (for additional features)
const remaining = parsed.result.remaining; // Unparsed/remaining args
const subcommand = parsed.result.subcommand; // If subcommand was used

When to Use Each Approach

Use CaseRecommended Approach
Simple config with flags and optionsparseInto / derive
Need choices or validationArgumentParser with addOption
Multi-value options (-I a -I b)ArgumentParser with addAppend
SubcommandsArgumentParser with addSubcommand
Rapid prototypingparseInto / derive
Production CLI with all featuresArgumentParser (full control)
Mix of simple and complexHybrid (use both)

Released under the MIT License.