From 0249f3c174907ca8fdfe966c98c2a355d760e68f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Aug 2022 11:54:51 -0700 Subject: [PATCH] cli parsing supports modification, add "RepeatableString" as example This lets values modify themselves, which we use to make a repeatable string implementation. We will use this initially to specify config files to load. --- src/cli_args.zig | 64 ++++++++++++++++++++++++++++-------------------- src/config.zig | 49 +++++++++++++++++++++++++++++++----- src/main.zig | 1 + 3 files changed, 82 insertions(+), 32 deletions(-) diff --git a/src/cli_args.zig b/src/cli_args.zig index a13b7880d..b68f8b072 100644 --- a/src/cli_args.zig +++ b/src/cli_args.zig @@ -61,36 +61,48 @@ fn parseIntoField( inline for (info.Struct.fields) |field| { if (mem.eql(u8, field.name, key)) { - @field(dst, field.name) = field: { - // For optional fields, we just treat it as the child type. - // This lets optional fields default to null but get set by - // the CLI. - const Field = switch (@typeInfo(field.field_type)) { - .Optional => |opt| opt.child, - else => field.field_type, - }; - const fieldInfo = @typeInfo(Field); + // For optional fields, we just treat it as the child type. + // This lets optional fields default to null but get set by + // the CLI. + const Field = switch (@typeInfo(field.field_type)) { + .Optional => |opt| opt.child, + else => field.field_type, + }; + const fieldInfo = @typeInfo(Field); - // If the type implements a parse function, call that. - // NOTE(mitchellh): this is a pretty nasty break statement. - // I split it into two at first and it failed with Zig as of - // July 21, 2022. I think stage2+ will fix this so lets clean - // this up when that comes out. - break :field if (fieldInfo == .Struct and @hasDecl(Field, "parseCLI")) - try Field.parseCLI(value) - else switch (Field) { - []const u8 => if (value) |slice| value: { - const buf = try alloc.alloc(u8, slice.len); - mem.copy(u8, buf, slice); - break :value buf; - } else return error.ValueRequired, + // If we are a struct and have parseCLI, we call that and use + // that to set the value. + if (fieldInfo == .Struct and @hasDecl(Field, "parseCLI")) { + const fnInfo = @typeInfo(@TypeOf(Field.parseCLI)).Fn; + switch (fnInfo.args.len) { + // 1 arg = (input) => output + 1 => @field(dst, field.name) = try Field.parseCLI(value), - bool => try parseBool(value orelse "t"), + // 2 arg = (self, input) => void + 2 => try @field(dst, field.name).parseCLI(value), - u8 => try std.fmt.parseInt(u8, value orelse return error.ValueRequired, 0), + // 3 arg = (self, alloc, input) => void + 3 => try @field(dst, field.name).parseCLI(alloc, value), - else => unreachable, - }; + else => @compileError("parseCLI invalid argument count"), + } + + return; + } + + // No parseCLI, magic the value based on the type + @field(dst, field.name) = switch (Field) { + []const u8 => if (value) |slice| value: { + const buf = try alloc.alloc(u8, slice.len); + mem.copy(u8, buf, slice); + break :value buf; + } else return error.ValueRequired, + + bool => try parseBool(value orelse "t"), + + u8 => try std.fmt.parseInt(u8, value orelse return error.ValueRequired, 0), + + else => unreachable, }; return; diff --git a/src/config.zig b/src/config.zig index a77c1f1fe..efd89caab 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; pub const Config = struct { @@ -15,6 +16,9 @@ pub const Config = struct { /// it'll be looked up in the PATH. command: ?[]const u8 = null, + /// Additional configuration files to read. + @"config-file": RepeatableString = .{}, + /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, @@ -65,13 +69,46 @@ pub const Color = struct { return result; } + + test "fromHex" { + const testing = std.testing; + + try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.fromHex("#000000")); + try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C")); + try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C")); + try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF")); + } }; -test "Color.fromHex" { - const testing = std.testing; +/// RepeatableString is a string value that can be repeated to accumulate +/// a list of strings. This isn't called "StringList" because I find that +/// sometimes leads to confusion that it _accepts_ a list such as +/// comma-separated values. +pub const RepeatableString = struct { + const Self = @This(); - try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.fromHex("#000000")); - try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C")); - try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C")); - try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF")); + // Allocator for the list is the arena for the parent config. + list: std.ArrayListUnmanaged([]const u8) = .{}, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + try self.list.append(alloc, value); + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.parseCLI(alloc, "B"); + + try testing.expectEqual(@as(usize, 2), list.list.items.len); + } +}; + +test { + std.testing.refAllDecls(@This()); } diff --git a/src/main.zig b/src/main.zig index be4b633d9..f958ebc03 100644 --- a/src/main.zig +++ b/src/main.zig @@ -43,6 +43,7 @@ pub fn main() !void { break :config result; }; defer config.deinit(); + log.info("config={}", .{config}); // We want to log all our errors glfw.setErrorCallback(glfwErrorCallback);