Skip to content

muhammad-fiaz/args.zig

cover

Documentation Zig Version GitHub stars GitHub issues GitHub pull requests GitHub last commit License CI Supported Platforms CodeQL Release Latest Release Sponsor GitHub Sponsors Repo Visitors

A fast, powerful, and developer-friendly command-line argument parsing library for Zig.

Documentation | API Reference | Quick Start | Contributing


A production-grade, high-performance command-line argument parsing library for Zig, inspired by Python's argparse with a clean, intuitive, and developer-friendly API.

Note

args.zig is a relatively new project, but it is designed and tested with production use in mind. The API is intended to be stable, and the library focuses on performance, correctness, and real-world CLI needs.

If you love args.zig, make sure to give it a star!

Features

Release Installation (Recommended)

Install the latest stable release (v0.0.4):

zig fetch --save https://github.com/muhammad-fiaz/args.zig/archive/refs/tags/0.0.4.tar.gz

Install the previous stable release (v0.0.3):

zig fetch --save https://github.com/muhammad-fiaz/args.zig/archive/refs/tags/0.0.3.tar.gz

Nightly Installation

Install the latest development version:

zig fetch --save git+https://github.com/muhammad-fiaz/args.zig

Configure build.zig

Then add it to your build.zig:

const args_dep = b.dependency("args", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("args", args_dep.module("args"));

Quick Start

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

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

    // Create argument parser
    var parser = try args.ArgumentParser.init(allocator, .{
        .name = "myapp",
        .version = "1.0.0",
        .description = "A sample application built with args.zig",
    });
    defer parser.deinit();

    // Add arguments
    try parser.addFlag("verbose", .{
        .short = 'v',
        .help = "Enable verbose output",
    });

    try parser.addOption("output", .{
        .short = 'o',
        .help = "Output file path",
        .default = "output.txt",
    });

    try parser.addPositional("input", .{
        .help = "Input file to process",
    });

    // Parse command-line arguments
    var result = try parser.parseProcess();
    defer result.deinit();

    // Use parsed values
    const verbose = result.getBool("verbose") orelse false;
    const output = result.getString("output") orelse "output.txt";
    const input = result.getString("input") orelse "unknown";

    if (verbose) {
        std.debug.print("Processing {s} -> {s}\n", .{ input, output });
    }
}

Examples

Brief usage pattern:

var parser = try args.ArgumentParser.init(allocator, .{ .name = "app" });
defer parser.deinit();
try parser.addFlag("verbose", .{ .short = 'v' });
try parser.addOption("output", .{ .short = 'o' });
var result = try parser.parseProcess();
defer result.deinit();

Flags and Options

// Boolean flag
try parser.addFlag("verbose", .{ .short = 'v', .help = "Verbose mode" });

// String option
try parser.addOption("config", .{ .short = 'c', .help = "Config file" });

// Integer option
try parser.addOption("count", .{
    .short = 'n',
    .value_type = .int,
    .default = "10",
});

// Choice option
try parser.addOption("format", .{
    .short = 'f',
    .choices = &[_][]const u8{ "json", "xml", "csv" },
});

Counter Arguments

// -v, -vv, -vvv for increasing verbosity
try parser.addCounter("verbose", .{ .short = 'v' });

var result = try parser.parse(&[_][]const u8{ "-v", "-v", "-v" });
const verbosity = result.get("verbose").?.counter; // = 3

Subcommands

try parser.addSubcommand(.{
    .name = "clone",
    .help = "Clone a repository",
    .args = &[_]args.ArgSpec{
        .{ .name = "url", .positional = true, .required = true },
        .{ .name = "depth", .short = 'd', .long = "depth", .value_type = .int },
    },
});

try parser.addSubcommand(.{
    .name = "init",
    .help = "Initialize a new repository",
});

Shell Completions

// Generate Bash completion script
const bash_script = try parser.generateCompletion(.bash);
std.debug.print("{s}", .{bash_script});

// Also supports: .zsh, .fish, .powershell, .nushell

Environment Variable Fallback

try parser.addOption("token", .{
    .help = "API token",
    .env_var = "API_TOKEN",  // Falls back to $API_TOKEN
});

Typed Input Validation Helpers

Use dedicated helpers for common API and configuration inputs:

try parser.addEmailOption("email", .{ .short = 'e', .required = true, .env_var = "APP_EMAIL" });
try parser.addUrlOption("endpoint", .{});
try parser.addIpv4Option("host", .{});
try parser.addIpOption("host-any", .{}); // IPv4 or IPv6
try parser.addIpv6Option("host-v6", .{});
try parser.addHostNameOption("hostname", .{});
try parser.addPortOption("port", .{});
try parser.addEndpointOption("service", .{}); // host:port
try parser.addKeyValueOption("label", .{});   // key=value
try parser.addUuidOption("request-id", .{});
try parser.addIsoDateOption("run-date", .{});
try parser.addIsoDateTimeOption("timestamp", .{});
try parser.addYearOption("year", .{});
try parser.addTimeOption("time", .{});
try parser.addAbsolutePathOption("workspace", .{});
try parser.addJsonOption("payload", .{});

try parser.addOption("retries", .{
    .value_type = .int,
    .validator = args.Validators.intRange(1, 10),
    .default = "3",
});

try parser.addOption("peer", .{
    .validator = args.Validators.anyIp,
});

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

const email = result.getString("email") orelse "";
const service = result.getString("service") orelse "localhost:8080";
const retries = result.getInt("retries") orelse 3;
const label = result.getKeyValue("label");

Negated Long Flags

Long boolean flags support --no-<name> by default:

try parser.addFlag("cache", .{ .help = "Enable cache" });

var result = try parser.parse(&[_][]const u8{"--no-cache"});
defer result.deinit();

const cache_enabled = result.getBool("cache") orelse true; // false

Inverse Boolean Flags

Use addFalseFlag when your primary option semantics are "disable this behavior":

try parser.addFalseFlag("color", .{ .help = "Disable color output" });

var result = try parser.parse(&[_][]const u8{"--color"});
defer result.deinit();

const color_enabled = result.getBool("color") orelse true; // false

CMD-Style Select And All

Use helpers to quickly model common command patterns:

try parser.addSelectOrAllCsv(.{
    .select_short = 's',
    .all_short = 'a',
});

This creates an exclusive pair (--select <csv-list> vs --all).

Normalize selections into canonical values:

var resolved = try args.resolveSelectOrAllStrict(allocator, &result, .{
    .choices = &[_][]const u8{ "users", "groups", "logs" },
    .allow_prefix_match = true,
    .dedupe = true,
});
defer resolved.deinit();

Question-Based Selection Flow

Resolve selection from parsed args or ask the user when missing:

const decision = try args.resolveSelectOrAllWithPrompt(allocator, &parsed, .{
    .question = "Select target",
    .choices = &[_][]const u8{ "users", "groups", "logs" },
    .default_choice = "users",
    .allow_all = true,
});

Include/Exclude Filters

Use reusable helpers for filter-style commands:

try parser.addIncludeExclude(.{ .include_short = 'i', .exclude_short = 'x' });

var parsed = try parser.parseProcess();
defer parsed.deinit();

var filters = try args.resolveIncludeExclude(allocator, &parsed, "include", "exclude");
defer filters.deinit();

For stricter behavior (choice normalization, deduplication, and conflict checks):

var strict_filters = try args.resolveIncludeExcludeStrict(allocator, &parsed, .{
    .choices = &[_][]const u8{ "users", "groups", "logs" },
    .all_keyword = "all",
});
defer strict_filters.deinit();

File And Extension Support

Use dedicated helpers for path/file/directory workflows:

try parser.addFileOptionWithExtensions("input", &[_][]const u8{ "json", "yaml", "toml" }, .{
    .short = 'i',
    .must_exist = false,
});

try parser.addDirectoryOption("workspace", .{
    .short = 'w',
    .must_exist = false,
});

const output_name_validator = args.Validators.filePolicy(&[_][]const u8{"json"}, false, 3, 64);

try parser.addFileNameOption("output-name", .{
    .short = 'o',
    .validator = output_name_validator,
});

You can still compose validators manually when needed:

const custom_validator = args.Validators.all(&[_]args.ValidatorFn{
    args.Validators.fileName,
    args.Validators.fileNameLength(3, 64),
});

Argument Groups

// Create a named group
try parser.addArgumentGroup("Server Options", .{
    .description = "Configuration for the server",
});

// Arguments added after will belong to this group
try parser.addOption("host", .{ .help = "Bind address" });
try parser.addOption("port", .{ .value_type = .int, .help = "Port number" });

// Reset to default (ungrouped)
parser.setGroup(null);

Mutually Exclusive Groups

try parser.addArgumentGroup("Mode", .{
    .exclusive = true,
    .required = true, // User MUST choose exactly one
});

try parser.addFlag("interactive", .{ .short = 'i' });
try parser.addFlag("batch", .{ .short = 'b' });

Custom Validation

fn validateUser(val: []const u8) args.validation.ValidationResult {
    if (val.len < 3) return .{ .err = "username too short" };
    return .{ .ok = {} };
}

try parser.addOption("user", .{
    .help = "Username",
    .validator = validateUser,
});

// See examples/custom_parsing.zig for complex format validation
// e.g. --mode 1920x1080@60Hz
try parser.addOption("mode", .{
    .help = "Display mode",
    .validator = validateMode,
    .metavar = "<W>x<H>[@<R>Hz]",
});

Aliases

You can define multiple names (aliases) for a single argument:

try parser.addArg(.{
    .name = "verbose",
    .long = "verbose",
    .aliases = &[_][]const u8{ "v", "loud", "debug" },
    .action = .store_true,
    .help = "Enable verbose output",
});

Callbacks

Trigger a function immediately when an argument is parsed:

fn onOutput(name: []const u8, value: ?[]const u8) void {
    std.debug.print("Option {s} received value: {s}\n", .{name, value orelse "null"});
}

// ...

try parser.addArg(.{
    .name = "output",
    .long = "output",
    .action = .callback,
    .callback = onOutput,
});

Declarative Structs

Define your CLI interface using a native Zig struct:

const Config = struct {
    verbose: bool,
    output: ?[]const u8,
    count: i32,
};

// Parse directly into the struct
var parsed = try args.parseInto(allocator, Config, .{
    .name = "myapp",
}, null);
defer parsed.deinit();

std.debug.print("Count: {d}\n", .{parsed.options.count});

Configuration

Update Checker

The update checker is enabled by default to keep you informed about new features and fixes. To disable it:

// Method 1: Global disable (Recommended)
args.disableUpdateCheck();

// Method 2: Per-parser configuration
var parser = try args.ArgumentParser.init(allocator, .{
    .name = "myapp",
    .config = .{ .check_for_updates = false },
});

Minimal Configuration

var parser = try args.ArgumentParser.init(allocator, .{
    .name = "myapp",
    .config = args.Config.minimal(), // No colors, no update check
});

Building

# Build library
zig build

# Run tests
zig build test

# Run all examples in one go
zig build run-all-examples

# Run examples
zig build run-basic
zig build run-advanced
zig build run-config_modes
zig build run-negated_flags
zig build run-positional_validation
zig build run-select_all
zig build run-question_flow
zig build run-include_exclude
zig build run-include_exclude_strict
zig build run-file_support
zig build run-data_input_validation
zig build run-network_endpoints
zig build run-error_handling
zig build run-subcommand_suggestions
zig build run-decryption_options
zig build run-update_check

# Run benchmarks
zig build bench

# Format code
zig build fmt

Cross-Platform Validation

Use the following commands to validate target coverage:

# Native target tests (runs tests)
zig build test

# Cross-target compile validation (builds all artifacts for each target)
zig build -Dtarget=x86_64-windows-gnu
zig build -Dtarget=x86_64-linux-gnu
zig build -Dtarget=aarch64-macos

# Targeted test invocations (must run on matching host/runner)
zig build test -Dtarget=x86_64-windows-gnu
zig build test -Dtarget=x86_64-linux-gnu
zig build test -Dtarget=aarch64-macos

On Windows hosts, Linux/macOS test binaries can be compiled but not executed directly. Run those test commands on Linux/macOS CI runners (or native machines) for full runtime verification.

Benchmarks

Run benchmarks to see the performance:

zig build bench

Benchmark Results

Typical results on modern hardware (10,000 iterations):

Benchmark Avg Time Throughput
Simple Flags (3 flags) ~33 μs ~30,000 ops/sec
Multiple Options (3 options) ~34 μs ~29,200 ops/sec
Positional Arguments ~24 μs ~40,700 ops/sec
Counters (-vvv -dd) ~24 μs ~41,800 ops/sec
Subcommands (2 subcommands) ~23 μs ~43,500 ops/sec
Mixed Arguments (complex CLI) ~40 μs ~24,600 ops/sec
Argument Groups ~23 μs ~42,900 ops/sec
Callbacks ~23 μs ~42,400 ops/sec
Negated Flags ~22 μs ~45,000 ops/sec
Select/All Helpers ~25 μs ~39,500 ops/sec
Select/All CSV Strict Resolve ~53 μs ~18,800 ops/sec
Include/Exclude Strict Resolve ~31 μs ~31,800 ops/sec
Prompt Resolution (Parsed) ~24 μs ~41,600 ops/sec
Suggestion Lookup ~2 μs ~500,000 ops/sec
Subcommand Suggestion Lookup ~2 μs ~500,000 ops/sec
Help Text Generation ~46 μs ~21,500 ops/sec
Shell Completion Generation (Bash) ~23 μs ~43,300 ops/sec
Shell Completion Generation (Zsh) ~24 μs ~41,900 ops/sec
Declarative Structs ~29 μs ~34,600 ops/sec
Expect Validation ~18 μs ~56,400 ops/sec
File Extension Validation ~21 μs ~47,100 ops/sec
File Name Policy Validation ~22 μs ~46,200 ops/sec
Typed Input Validation ~138 μs ~7,300 ops/sec
Decryption Option (Base64) ~30 μs ~33,000 ops/sec

Note

Results vary based on hardware and system load. Tested on Windows x86_64 with Zig 0.15.0. If you want the latest release benchmarks, you can find them on the repository releases.

Documentation

Full documentation is available at muhammad-fiaz.github.io/args.zig.

Contributing

Contributions are welcome! Please read our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

See our Code of Conduct for community guidelines.

Security

For security concerns, please see our Security Policy.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

If you find this project helpful, consider supporting it: