SkillAgentSearch skills...

Zargs

Comptime Argparse for Zig! Let's start to build your command line!

Install / Use

/learn @kioz-wang/Zargs

README

zargs

other language: 中文简体

Another Comptime-argparse for Zig! Let's start to build your command line!

badge x86_64-linux badge aarch64-linux badge x86_64-windows badge x86_64-macos badge aarch64-macos

asciicast

const std = @import("std");
const zargs = @import("zargs");
const Command = zargs.Command;
const Arg = zargs.Arg;
const Ranges = zargs.Ranges;
const ztype = @import("ztype");
const String = ztype.String;

pub fn main() !void {
    // Like Py3 argparse, https://docs.python.org/3.13/library/argparse.html
    const remove = Command.new("remove")
        .about("Remove something")
        .alias("rm").alias("uninstall").alias("del")
        .opt("verbose", u32, .{ .short = 'v' })
        .optArg("count", u32, .{ .short = 'c', .argName = "CNT", .default = 9 })
        .posArg("name", String, .{});

    // Like Rust clap, https://docs.rs/clap/latest/clap/
    const cmd = Command.new("demo").requireSub("action")
        .about("This is a demo intended to be showcased in the README.")
        .author("KiozWang")
        .homepage("https://github.com/kioz-wang/zargs")
        .arg(Arg.opt("verbose", u32).short('v').help("help of verbose"))
        .arg(Arg.optArg("logfile", ?ztype.OpenLazy(.fileCreate, .{ .read = true })).long("log").help("Store log into a file"))
        .sub(Command.new("install")
            .about("Install something")
            .arg(Arg.optArg("count", u32).default(10)
                .short('c').short('n').short('t')
                .long("count").long("cnt")
                .ranges(Ranges(u32).new().u(5, 7).u(13, null)).choices(&.{ 10, 11 }))
            .arg(Arg.posArg("name", String).rawChoices(&.{ "gcc", "clang" }))
            .arg(Arg.optArg("output", String).short('o').long("out"))
            .arg(Arg.optArg("vector", ?@Vector(3, i32)).long("vec")))
        .sub(remove);

    var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
    const allocator = gpa.allocator();

    var args = cmd.config(.{ .style = .classic }).parse(allocator) catch |e|
        zargs.exitf(e, 1, "\n{s}\n", .{cmd.usageString()});
    defer cmd.destroy(&args, allocator);
    if (args.logfile) |logfile| std.debug.print("Store log into {}\n", .{logfile});
    switch (args.action) {
        .install => |a| {
            std.debug.print("Installing {s}\n", .{a.name});
        },
        .remove => |a| {
            std.debug.print("Removing {s}\n", .{a.name});
            std.debug.print("{any}\n", .{a});
        },
    }
    std.debug.print("Success to do {s}\n", .{@tagName(args.action)});
}

Background

As a system level programming language, there should be an elegant solution for parsing command line arguments.

zargs draws inspiration from the API styles of Py3 argparse and Rust clap. It provides all parameter information during editing, reflects the parameter structure and parser at compile time, along with everything else needed, and supports dynamic memory allocation for parameters at runtime.

Installation

fetch

Get the latest version:

zig fetch --save git+https://github.com/kioz-wang/zargs

To fetch a specific version (e.g., v0.14.3):

zig fetch --save https://github.com/kioz-wang/zargs/archive/refs/tags/v0.14.3.tar.gz

Version Notes

See https://github.com/kioz-wang/zargs/releases

The version number follows the format vx.y.z[-alpha.n]:

  • x: Currently fixed at 0. It will increment to 1 when the project stabilizes. Afterward, it will increment by 1 for any breaking changes.
  • y: Represents the supported Zig version. For example, vx.14.z supports Zig 0.14.0.
  • z: Iteration version, indicating releases with new features or significant changes (see milestones).
  • n: Minor version, indicating releases with fixes or minor updates.

Importing Core Module

In your build.zig, use addImport (for example):

const exe = b.addExecutable(.{
    .name = "your_app",
    .root_source_file = b.path("src/main.zig"),
    .target = b.standardTargetOptions(.{}),
    .optimize = b.standardOptimizeOption(.{}),
});
exe.root_module.addImport("zargs", b.dependency("zargs", .{}).module("zargs"));
b.installArtifact(exe);

const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
    run_cmd.addArgs(args);
}

const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);

After importing in your source code, you will gain access to the following features:

  • Command and argument builders: Command, Arg
  • Versatile iterator support: TokenIter
  • Convenient exit functions: exit, exitf

See the documentation for details.

const zargs = @import("zargs");

Importing other modules

In addition to the core module zargs, I also exported the fmt and par modules.

fmt

any, which provides a more flexible and powerful formatting scheme.

stringify, if a class contains a method such as fname(self, writer), then you can obtain a compile-time string like this:

pub fn getString(self: Self) *const [stringify(self, “fname”).count():0]u8 {
    return stringify(self, “fname”).literal();
}

comptimeUpperString converts a compile-time string to uppercase.

par

any, parses the string into any type instance you want.

For struct, you need to implement pub fn parse(s: String, a_maybe: ?Allocator) ?Self. For enum, the default parser is std.meta.stringToEnum, but if parse is implemented, it will be used instead.

destroy, releases the parsed type instance.

Safe release: for instances where no memory allocation occurred during parsing, no actual release action is performed. For struct and enum, actual release actions are performed only when pub fn destroy(self: Self, a: Allocator) void is implemented.

ztype

Provides String, LiteralString and checker.

Provides the wrappers of some struct in std:

  • Open/OpenLazy(...): std.fs.File/Dir
  • ...

Features

Options, Arguments, Subcommands

Terminology

  • Option (opt)
    • Single Option (singleOpt)
      • Boolean Option (boolOpt), T == bool
      • Accumulative Option (repeatOpt), @typeInfo(T) == .int
    • Option with Argument (argOpt)
      • Option with Single Argument (singleArgOpt), T, ?T
      • Option with Fixed Number of Arguments (arrayArgOpt), [n]T
      • Option with Variable Number of Arguments (multiArgOpt), []T
  • Argument (arg)
    • Option Argument (optArg) (equivalent to Option with Argument)
    • Positional Argument (posArg)
      • Single Positional Argument (singlePosArg), T, ?T
      • Fixed Number of Positional Arguments (arrayPosArg), [n]T
  • Subcommand (subCmd)

Matching and Parsing

Matching and parsing are driven by an iterator. For options, the option is always matched first, and if it takes an argument, the argument is then parsed. For positional arguments, parsing is attempted directly.

For arguments, T must be the smallest parsable unit: []const u8 -> T

  • .int
  • .float
  • .bool
    • true: 'y', 't', "yes", "true" (case insensitive)
    • false: 'n', 'f', "no", "false" (case insensitive)
  • .enum: Uses std.meta.stringToEnum by default, but parse method takes priority
  • .struct: Struct with parse method
  • .vector
    • Only supports base types of .int, .float, and .bool
    • @Vector{1,1}: [\(\[\{][ ]*1[ ]*[;:,][ ]*1[ ]*[\)\]\}]
    • @Vector{true,false}: [\(\[\{][ ]*y[ ]*[;:,][ ]*no[ ]*[\)\]\}]

If type T has no associated default parser or parse method, you can specify a custom parser (.parseFn) for the parameter. Obviously, single-option parameters cannot have parsers as it would be meaningless.

Default Values and Optionality

Options and arguments can be configured with default values (.default). Once configured, the option or argument becomes optional.

  • Even if not explicitly configured, single options always have default values: boolean options default to false, and accumulative options default to 0.
  • Options or arguments with an optional type ?T cannot be explicitly configured: they are forced to default to null.

Single options, options with a single argument of optional type, or single positional arguments of optional type are always optional.

Default values must be determined at comptime. For argOpt and posArg, if the value cannot be determined at comptime (e.g., std.fs.cwd() at Windows), you can configure the default input (.rawDefault), which will determine the default value in the perser.

Value Ranges

Value ranges (.ranges, .choices) can be configured for arguments, which are validated after parsing.

Default values are not validated (intentional feature? 😄)

If constructing value ranges is cumbersome, .rawChoices can be used to filter values before parsing.

Ranges

Related Skills

View on GitHub
GitHub Stars30
CategoryDevelopment
Updated7h ago
Forks3

Languages

Zig

Security Score

95/100

Audited on Apr 4, 2026

No findings