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!