Sometimes, you are writing a little tool or just prototyping something and you don’t want to spend to much time creating a build.zig
or configure a dependency. You just want to get something up and move on with your day.
But argument parsing is bothersome. It’s not exactly easy to write something quickly, but you don’t want to spend to much time on it either.
So I came up with my solution that fits a niche in that problem space. A simple 250 lines solution that you can just copy paste somewhere in your project.
Here is comes:
// A very simple command line argument parser.
//
// The purpose is to be simple, modifiable and easily embeddable. Just
// copy-paste is into your project and that's it.
//
// Usage:
// ```zig
// // Assuming you put the code into clap.zig
// const clap = @import("clap.zig");
//
// const args = try std.process.argsAlloc(allocator);
// defer std.process.argsFree(allocator, args);
//
// const parsedArgs = clap.parser(clap.ArgDescriptor{
// .name = "qvm",
// .description = "A QuakeC virtual machine",
// .withHelp = true,
// .version = "0.1.0",
// .expectArgs = &[_][]const u8{ "datfile" },
// .options = &[_]clap.OptionDescription{ .{
// .short = "t",
// .long = "trace",
// .help = "Enable tracing of instructions",
// }, .{
// .short = "e",
// .long = "verbose",
// .help = "Display additional information about the VM",
// }, .{
// .short = "m",
// .long = "memory-size",
// .arg = .{ .name = "memory", .type = []const u8 },
// .help = "Amount of memory to allocate for the VM (-m 12, -m 64K, -m 1M)",
// }, .{
// .short = "j",
// .long = "jump-to",
// .arg = .{ .name = "function", .type = []const u8 },
// .help = "Jump to function on startup",
// }, .{
// .short = "b",
// .long = "bsp-file",
// .arg = .{ .name = "bspfile", .type = []const u8 },
// .help = "Load a BSP file",
// }, .{
// .short = "r",
// .long = "run",
// .help = "Run the event loop (triggering the nextthink timers)",
// } },
// }).parse(args);
//
// const filepath = parsedArgs.arguments.items[0];
// const memsize = if (args.getOption([]const u8, "memory-size")) |memsizeArg| blkinner: {
// const lastChar = memsizeArg[memsizeArg.len - 1];
// break :blkinner switch (lastChar) {
// 'k', 'K' => try std.fmt.parseInt(usize, memsizeArg[0..memsizeArg.len - 1], 10) * 1024,
// 'm', 'M' => try std.fmt.parseInt(usize, memsizeArg[0..memsizeArg.len - 1], 10) * 1024 * 1024,
// else => try std.fmt.parseInt(usize, memsizeArg, 10),
// };
// } else 1024 * 1024 * 1; // 1Mb by default;
// // Create the VM
// var vm = try VM.init(allocator, .{
// .entries = null,
// }, .{
// .trace = parsedArgs.getSwitch("trace"),
// .memsize = memsize,
// .verbose = parsedArgs.getSwitch("verbose"),
// });
// defer vm.deinit();
// ```
// Running with the `--help` option will show:
// ```
// qvm (0.1.0) A QuakeC virtual machine
// Usage: qvm [OPTIONS] datfile
//
// Options:
// -t,--trace Enable tracing of instructions
// -e,--verbose Display additional information about the VM
// -m,--memory-size memory
// Amount of memory to allocate for the VM (-m 12, -m 64K, -m 1M)
// -j,--jump-to function
// Jump to function on startup
// -b,--bsp-file bspfile
// Load a BSP file
// -r,--run Run the event loop (triggering the nextthink timers)
//
// ```
const std = @import("std");
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
pub const OptionDescription = struct {
short: ?[]const u8,
long: []const u8,
arg: ?struct { name: []const u8, type: type } = null,
help: []const u8,
};
pub const ArgDescriptor = struct {
bufferSize: usize = 1024,
name: []const u8,
description: ?[]const u8 = null,
withHelp: bool = true,
version: ?[]const u8 = null,
expectArgs: []const []const u8 = &[_][]const u8{},
options: []const OptionDescription,
};
pub fn findOption(comptime T: type, value: anytype, argsInfo: std.builtin.Type.Struct,
name: []const u8) ?type {
inline for (argsInfo.fields) |field| {
if (std.mem.eql(u8, field.name, name) and field.type == T) {
return @field(value, field.name);
}
}
return null;
}
pub fn printUsage(allocator: std.mem.Allocator, argsDescriptor: ArgDescriptor) void {
stdout.print("Usage: {s}{s}{s}\n", .{
argsDescriptor.name,
if (argsDescriptor.options.len > 0) " [OPTIONS]" else "",
if (argsDescriptor.expectArgs.len > 0) blk: {
const argsStr = std.mem.join(allocator, " ", argsDescriptor.expectArgs)
catch @panic("increase fixed buffer size");
break :blk std.fmt.allocPrint(allocator, " {s}", .{ argsStr })
catch @panic("increase fixed buffer size");
} else "",
}) catch unreachable;
}
pub fn printHelp(allocator: std.mem.Allocator, argsDescriptor: ArgDescriptor) void {
stdout.print("{s}{s} {s}\n", .{
argsDescriptor.name,
if (argsDescriptor.version) |version| " (" ++ version ++ ")" else "",
argsDescriptor.description orelse "",
}) catch unreachable;
stdout.print("\n", .{}) catch unreachable;
printUsage(allocator, argsDescriptor);
stdout.print("\nOptions:\n", .{}) catch unreachable;
inline for (argsDescriptor.options) |option| {
var buffer: [argsDescriptor.bufferSize]u8 = undefined;
const printed = std.fmt.bufPrint(&buffer, " {s}{s}{s}", .{
if (option.short) |short| "-" ++ short ++ "," else " ",
"--" ++ option.long,
if (option.arg) |arg| " " ++ arg.name else "",
}) catch @panic("increase fixed buffer size");
if (printed.len > 23) {
stdout.print("{s}\n {s}\n", .{ printed, option.help })
catch unreachable;
} else {
stdout.print("{s: <24} {s}\n", .{ printed, option.help }) catch unreachable;
}
}
}
pub const Args = struct {
const Self = @This();
switchMap: std.StringHashMap(bool),
optionMap: std.StringHashMap([]const u8),
arguments: std.ArrayList([]const u8),
pub fn getSwitch(self: Self, name: []const u8) bool {
return self.switchMap.get(name) orelse false;
}
pub fn getOption(self: Self, comptime T: type, name: []const u8) ?T {
return self.optionMap.get(name);
}
};
pub fn parser(argsDescriptor: ArgDescriptor) type {
return struct {
var buffer: [argsDescriptor.bufferSize]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
var argsStore = Args{
.switchMap = std.StringHashMap(bool).init(allocator),
.optionMap = std.StringHashMap([]const u8).init(allocator),
.arguments = std.ArrayList([]const u8).init(allocator),
};
pub fn parse(args: [][:0]u8) Args {
if (argsDescriptor.withHelp) {
// Look for help and print it
for (args) |arg| {
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
printHelp(allocator, argsDescriptor);
std.posix.exit(0);
}
}
var i: u16 = 1;
while (i < args.len) {
const arg = args[i];
if (arg[0] == '-') {
// Handle option in the block. i might be incremented additionally
// it the option expects an argument.
inline for (argsDescriptor.options) |option| {
if ((option.short != null and std.mem.eql(u8, arg[1..], option.short.?)) or
std.mem.eql(u8, arg[2..], option.long)) {
argsStore.switchMap.put(option.long, true)
catch @panic("increase fixed buffer size");
if (option.arg) |optionArg| {
_ = optionArg;
// We have an argument to the option
if (i > args.len - 1 or args[i + 1][0] == '-') {
// Missing argument
stderr.print("error: option {s} expected an argument\n", .{ arg })
catch unreachable;
printUsage(allocator, argsDescriptor);
}
argsStore.optionMap.put(option.long, args[i + 1])
catch @panic("increase fixed buffer size");
i += 1;
}
break;
}
} else {
// An option was provided but not described.
stderr.print("error: unknown option {s}\n", .{ arg }) catch unreachable;
printUsage(allocator, argsDescriptor);
std.posix.exit(1);
}
} else {
// Here are the argument to the program.
argsStore.arguments.append(args[i]) catch unreachable;
}
i += 1;
}
}
if (argsStore.arguments.items.len != argsDescriptor.expectArgs.len) {
stderr.print("error: incorrect number of arguments. Expected {} arguments, {} given.\n", .{
argsDescriptor.expectArgs.len, argsStore.arguments.items.len,
}) catch unreachable;
printUsage(allocator, argsDescriptor);
std.posix.exit(1);
}
return argsStore;
}
};
}
It’s probably buggy and very incomplete but it does the job for small hacked together prototype and being very simple, you can grasp it in probably a few minutes and it can easily be modified to fit your particular need.
Let me know what you think!