Continuing my quest to write a command line arguments parser, I decided to start tackling subcommands.
For that I wrote the following test cases:
test "parse struct as first subcommand" {
const TestCmd = struct {
name: ?[]const u8,
opt_env: ?[]const u8,
opt_benchmark: bool,
};
const ShowCmd = struct {
name: []const u8,
};
const SubcommandTag = enum {
@"test",
show,
};
const Subcommand = union(SubcommandTag) {
@"test": TestCmd,
show: ShowCmd,
};
const Cmd = struct {
subcommand: Subcommand,
opt_verbose: bool,
};
var args = [_][]const u8{ "command_name", "test", "test-01", "--env", "production", "--benchmark" };
const expected = Cmd{
.subcommand = Subcommand{
.@"test" = TestCmd{
.name = "test-01",
.opt_env = "production",
.opt_benchmark = true,
},
},
.opt_verbose = false,
};
const actual = try parse(Cmd, &args);
try std.testing.expectEqual(expected, actual.?);
}
test "parse struct as second subcommand" {
const TestCmd = struct {
name: ?[]const u8,
opt_env: ?[]const u8,
opt_benchmark: bool,
};
const ShowCmd = struct {
name: []const u8,
};
const SubcommandTag = enum {
@"test",
show,
};
const Subcommand = union(SubcommandTag) {
@"test": TestCmd,
show: ShowCmd,
};
const Cmd = struct {
subcommand: Subcommand,
opt_verbose: bool,
};
var args = [_][]const u8{ "command_name", "show", "test-01" };
const expected = Cmd{
.subcommand = Subcommand{
.show = ShowCmd{
.name = "test-01",
},
},
.opt_verbose = false,
};
const actual = try parse(Cmd, &args);
try std.testing.expectEqual(expected, actual.?);
}
They both attempt to create the tagged union Subcommand by setting the proper field:
pub fn parse(comptime T: type, args: [][]const u8) !?T {
for (args) |arg| {
if (std.mem.eql(u8, arg, "--help")) {
try help_for(T, std.io.getStdOut().writer());
return null;
}
}
var r: T = undefined;
switch (@typeInfo(T)) {
.Union => |data| {
inline for (data.fields) |field| {
std.debug.print("field: {s} args: {s}\n", .{ field.name, args[1] });
if (std.mem.eql(u8, args[1], field.name)) {
// let's assemble the subcommand structure
const subcommand = try parse(field.type, args[1..]);
@field(r, field.name) = subcommand.?;
}
}
// ...
To my surprise, the first test case passes but the second gives me this error:
field: test args: show
field: show args: show
thread 3653919 panic: access of union field 'show' while field 'test' is active
/Users/fcoury/code/zig80/src/cli.zig:47:21: 0x102345cef in parse__anon_2993 (test)
@field(r, field.name) = subcommand.?;
^
I declaring the subcommand in this case as var r: T = undefined; and am not accessing it any time during my loop, so how is it that it thinks that test is active for it?