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).

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.

  • 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) → Required Option

Supported Field Types ​

Zig TypeCLI BehaviorExample Usage
boolFlag, defaults to false--verbose
?boolOptional flag--debug
[]const u8Required string--name value
?[]const u8Optional string--output file.txt
i32, i64Required integer--count 42
?i32, ?i64Optional integer--port 8080
f32, f64Required float--rate 0.5
?f64Optional float--timeout 30.5

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 (non-optional)
    count: i32,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var parsed = try args.parseInto(allocator, Config, .{
        .name = "myapp",
        .description = "Struct-based parsing demo",
    }, null);
    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});
    
    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

$ myapp --count 10 --output result.txt --dry-run --port 8080
Verbose: false
Dry Run: true
Count: 10
Output: result.txt
Port: 8080

Limitations and How to Handle Complex Cases ​

The parseInto approach is designed for simple to moderately complex CLIs. For advanced use cases, you can combine approaches:

Limitation 1: No Multi-Value Options ​

Struct-based parsing doesn't support repeatable options (like -I path1 -I path2). For this, use the traditional API:

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();
defer result.deinit();

// Access multiple values
const includes = result.getList("include"); // Returns slice of values

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 ​

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() !void {
    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();
    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);
var parsed = try args.derive(allocator, Config, options, null);

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);
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);
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.