Clarg - another take on command line arguments parsing

Hi everyone, let me introduce my first humble contribution to this beautiful ecosystem: clarg.
It’s another lib to parse command line arguments that I wrote to fit my needs. Only made possible by the power of Zig’s comptime.

The main goals were:

  • No allocation
  • Nested subcommands
  • Arguments default value

Basically defining arguments the looks like this:

const Op = enum { add, sub, mul, div };
const OpCmdArgs = struct {
    it_count: Arg(5) = .{ .desc = "iteration count", .short = 'i' },
    op: Arg(Op.add) = .{ .desc = "operation", .short = 'o' },
    help: Arg(bool) = .{ .short = 'h' },
};

const Size = enum { small, medium, large };
const Args = struct {
    // No default value
    print_ast: Arg(bool),

    // ------
    // Types
    //   You can specify the following types for arguments
    //   As no default value are specified, the resulting type when parsed will
    //   be ?T where T is the type inside `Arg(T)`
    t0: Arg(bool),
    t1: Arg(i64),
    t2: Arg(f64),
    t3: Arg([]const u8),
    // For strings there is also the enum literal .string that is supported
    t4: Arg(.string),
    // Enums
    t5: Arg(Size),

    // --------------
    // Default value
    //   You can use a value instead of a type to provide a fallback value
    //   Argument's type will be infered and the resulting type when parsed will
    //   be T where T is the type inside `Arg(T)`
    // Interger
    count: Arg(5) = .{ .desc = "iteration count", .short = 'c' },
    // Float
    delta: Arg(10.5) = .{ .desc = "delta time between calculations", .short = 'd', .required = true },
    // String
    dir_path: Arg("/home") = .{ .desc = "file path", .short = 'f' },
    // Enum
    other_size: Arg(Size.small) = .{ .desc = "size of binary" },

    // ------------
    // Positionals
    //   Positional arguments are defined using the `.positional` field and are parsed
    //   in the order of declaration. They can be define before and after other arguments
    file: Arg(.string) = .{ .desc = "file to run", .positional = true },
    outdir: Arg("/tmp") = .{ .desc = "output dir", .positional = true },

    // -------------
    // Sub-commands
    //   They are simply defined by giving a structure as argument's type
    cmd: Arg(OpCmdArgs) = .{ .desc = "operates on input" },

    // Description will be displayed
    pub const description =
        \\Description of the program
        \\it can be anything
    ;
};

With zig build run -- -h this prints:

Usage:
  basic [options] [args]
  basic [commands] [options] [args]

Description:
  Description of the program
  it can be anything

Commands:
  cmd                      operates on input

Arguments:
  <string>                 file to run
  <string>                 output dir [default: "/tmp"]

Options:
  --print-ast
  --t0
  --t1 <int>
  --t2 <float>
  --t3 <string>
  --t4 <string>
  --t5 <enum>
                             Supported values:
                               small
                               medium
                               large
  -c, --count <int>        iteration count [default: 5]
  -d, --delta <float>      delta time between calculations [default: 10.5]
  -f, --dir-path <string>  file path [default: "/home"]
  --other-size <enum>      size of binary [default: small]
                             Supported values:
                               small
                               medium
                               large
  -h, --help               Prints this help and exit

And with zig build run -- cmd -h:

Usage:
  basic [options] [args]

Options:
  -i, --it-count <int>  iteration count [default: 5]
  -o, --op <enum>       operation [default: add]
                          Supported values:
                            add
                            sub
                            mul
                            div
  -h, --help

And usage is fairly simple:

const clarg = @import("clarg");
const Arg = clarg.Arg;

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

    var diag: clarg.Diag = .empty;
    var args = try std.process.argsWithAllocator(gpa.allocator());
    defer args.deinit();

    const parsed = clarg.parse("basic", Args, &args, &diag, .{}) catch {
        try diag.reportToFile(.stderr());
        std.process.exit(1);
    };

    if (parsed.help) {
        try clarg.helpToFile(Args, .stderr());
        return;
    }

    // No default value are optionals (except bool that are false)
    if (parsed.t4) |val| {
        std.log.debug("T4 value: {s}", .{val});
    }

    // Required arguments aren't optional
    std.log.debug("Delta: {}", .{parsed.delta});

    // Default values are usable as is
    std.log.debug("count: {d}", .{parsed.count});
    std.log.debug("outdir: {s}", .{parsed.outdir});

    // Sub command usage
    if (parsed.cmd) |cmd| {
        if (cmd.help) {
            try clarg.helpToFile(OpCmdArgs, .stderr());
        }
    }
}

Hope this can be useful to someone!

18 Likes

Welcome to the Zig CLI club!

3 Likes

A CLI that takes advantage of zig comptime features. I like this kind of things.

1 Like

For those how decided to use it (who knows?), there’s a v0.1.1 release that fixes bugs found after the initial release (thanks @dacec354).
Changelog:

Features:

  • Add [required] for required arguments when printing help

Fixes:

  • It was possible to use positional arguments by their name
  • Crash when providing invalid arguments (like ‘-’)
  • Required positional arguments triggered an error even when provided

Cheers!

1 Like

Today, I’ve been going through bunch of CLI Parser library for ZX and ended up choosing ZLI.
I think I’m still lacking Array arg type that I really need.

Now I’m seeing another one haha. Need to compile a list of all the Zig CLI libs.

1 Like

Yes there are several good ones out there! Clarg doesn’t handle Array arguments yet unfortunately :slight_smile:

There’s a decent list here at zigistry.dev

That list also includes some “awesome compilation” repos with more.

@Zonion you can add tags like cli and clap to your repo to make it searchable in Zigistry.

2 Likes

Good to know thanks :slight_smile:

Hi everyone,
Quick update : version 0.1.2 was released yesterday introducing:

  • Multiple assignment operators. You can now choose between =, : and (the default being =)
  • No need to specify the program’s name anymore when using the parse function.

The only breaking change is the parse function, If needed you can read more in the release note (and look at the basic example to see usage).

2 Likes