properly copy string cli flags

This commit is contained in:
Mitchell Hashimoto
2022-05-19 15:49:26 -07:00
parent 57f257fd77
commit da359b8e36
3 changed files with 73 additions and 23 deletions

View File

@ -1,6 +1,8 @@
const std = @import("std"); const std = @import("std");
const mem = std.mem; const mem = std.mem;
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
// TODO: // TODO:
// - Only `--long=value` format is accepted. Do we want to allow // - Only `--long=value` format is accepted. Do we want to allow
@ -12,10 +14,18 @@ const assert = std.debug.assert;
/// the valid CLI flags. See the tests in this file as an example. For field /// the valid CLI flags. See the tests in this file as an example. For field
/// types that are structs, the struct can implement the `parseCLI` function /// types that are structs, the struct can implement the `parseCLI` function
/// to do custom parsing. /// to do custom parsing.
pub fn parse(comptime T: type, dst: *T, iter: anytype) !void { pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
const info = @typeInfo(T); const info = @typeInfo(T);
assert(info == .Struct); assert(info == .Struct);
// Make an arena for all our allocations if we support it. Otherwise,
// use an allocator that always fails.
const arena_alloc = if (@hasField(T, "_arena")) arena: {
dst._arena = ArenaAllocator.init(alloc);
break :arena dst._arena.?.allocator();
} else std.mem.fail_allocator;
errdefer if (@hasField(T, "_arena")) dst._arena.?.deinit();
while (iter.next()) |arg| { while (iter.next()) |arg| {
if (mem.startsWith(u8, arg, "--")) { if (mem.startsWith(u8, arg, "--")) {
var key: []const u8 = arg[2..]; var key: []const u8 = arg[2..];
@ -29,13 +39,23 @@ pub fn parse(comptime T: type, dst: *T, iter: anytype) !void {
break :value null; break :value null;
}; };
try parseIntoField(T, dst, key, value); try parseIntoField(T, arena_alloc, dst, key, value);
} }
} }
} }
/// Parse a single key/value pair into the destination type T. /// Parse a single key/value pair into the destination type T.
fn parseIntoField(comptime T: type, dst: *T, key: []const u8, value: ?[]const u8) !void { ///
/// This may result in allocations. The allocations can only be freed by freeing
/// all the memory associated with alloc. It is expected that alloc points to
/// an arena.
fn parseIntoField(
comptime T: type,
alloc: Allocator,
dst: *T,
key: []const u8,
value: ?[]const u8,
) !void {
const info = @typeInfo(T); const info = @typeInfo(T);
assert(info == .Struct); assert(info == .Struct);
@ -57,7 +77,11 @@ fn parseIntoField(comptime T: type, dst: *T, key: []const u8, value: ?[]const u8
// Otherwise infer based on type // Otherwise infer based on type
break :field switch (Field) { break :field switch (Field) {
[]const u8 => value orelse return error.ValueRequired, []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"), bool => try parseBool(value orelse "t"),
else => unreachable, else => unreachable,
}; };
@ -88,17 +112,21 @@ test "parse: simple" {
const testing = std.testing; const testing = std.testing;
var data: struct { var data: struct {
a: []const u8, a: []const u8 = "",
b: bool, b: bool = false,
@"b-f": bool, @"b-f": bool = true,
} = undefined;
_arena: ?ArenaAllocator = null,
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init( var iter = try std.process.ArgIteratorGeneral(.{}).init(
testing.allocator, testing.allocator,
"--a=42 --b --b-f=false", "--a=42 --b --b-f=false",
); );
defer iter.deinit(); defer iter.deinit();
try parse(@TypeOf(data), &data, &iter); try parse(@TypeOf(data), testing.allocator, &data, &iter);
try testing.expect(data._arena != null);
try testing.expectEqualStrings("42", data.a); try testing.expectEqualStrings("42", data.a);
try testing.expect(data.b); try testing.expect(data.b);
try testing.expect(!data.@"b-f"); try testing.expect(!data.@"b-f");
@ -106,57 +134,69 @@ test "parse: simple" {
test "parseIntoField: string" { test "parseIntoField: string" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct { var data: struct {
a: []const u8, a: []const u8,
} = undefined; } = undefined;
try parseIntoField(@TypeOf(data), &data, "a", "42"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
try testing.expectEqual(@as([]const u8, "42"), data.a); try testing.expectEqualStrings("42", data.a);
} }
test "parseIntoField: bool" { test "parseIntoField: bool" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct { var data: struct {
a: bool, a: bool,
} = undefined; } = undefined;
// True // True
try parseIntoField(@TypeOf(data), &data, "a", "1"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "1");
try testing.expectEqual(true, data.a); try testing.expectEqual(true, data.a);
try parseIntoField(@TypeOf(data), &data, "a", "t"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "t");
try testing.expectEqual(true, data.a); try testing.expectEqual(true, data.a);
try parseIntoField(@TypeOf(data), &data, "a", "T"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "T");
try testing.expectEqual(true, data.a); try testing.expectEqual(true, data.a);
try parseIntoField(@TypeOf(data), &data, "a", "true"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "true");
try testing.expectEqual(true, data.a); try testing.expectEqual(true, data.a);
// False // False
try parseIntoField(@TypeOf(data), &data, "a", "0"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "0");
try testing.expectEqual(false, data.a); try testing.expectEqual(false, data.a);
try parseIntoField(@TypeOf(data), &data, "a", "f"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "f");
try testing.expectEqual(false, data.a); try testing.expectEqual(false, data.a);
try parseIntoField(@TypeOf(data), &data, "a", "F"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "F");
try testing.expectEqual(false, data.a); try testing.expectEqual(false, data.a);
try parseIntoField(@TypeOf(data), &data, "a", "false"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "false");
try testing.expectEqual(false, data.a); try testing.expectEqual(false, data.a);
} }
test "parseIntoField: optional field" { test "parseIntoField: optional field" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct { var data: struct {
a: ?bool = null, a: ?bool = null,
} = .{}; } = .{};
// True // True
try parseIntoField(@TypeOf(data), &data, "a", "1"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "1");
try testing.expectEqual(true, data.a.?); try testing.expectEqual(true, data.a.?);
} }
test "parseIntoField: struct with parse func" { test "parseIntoField: struct with parse func" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct { var data: struct {
a: struct { a: struct {
@ -171,6 +211,6 @@ test "parseIntoField: struct with parse func" {
}, },
} = undefined; } = undefined;
try parseIntoField(@TypeOf(data), &data, "a", "42"); try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v); try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
} }

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
pub const Config = struct { pub const Config = struct {
/// Background color for the window. /// Background color for the window.
@ -10,6 +11,14 @@ pub const Config = struct {
/// The command to run, usually a shell. If this is not an absolute path, /// The command to run, usually a shell. If this is not an absolute path,
/// it'll be looked up in the PATH. /// it'll be looked up in the PATH.
command: ?[]const u8 = null, command: ?[]const u8 = null,
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
pub fn deinit(self: *Config) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
}; };
/// Color represents a color using RGB. /// Color represents a color using RGB.

View File

@ -16,13 +16,14 @@ pub fn main() !void {
const alloc = if (!tracy.enabled) gpa else tracy.allocator(gpa, null).allocator(); const alloc = if (!tracy.enabled) gpa else tracy.allocator(gpa, null).allocator();
// Parse the config from the CLI args // Parse the config from the CLI args
const config = config: { var config = config: {
var result: Config = .{}; var result: Config = .{};
var iter = try std.process.argsWithAllocator(alloc); var iter = try std.process.argsWithAllocator(alloc);
defer iter.deinit(); defer iter.deinit();
try cli_args.parse(Config, &result, &iter); try cli_args.parse(Config, alloc, &result, &iter);
break :config result; break :config result;
}; };
defer config.deinit();
// Initialize glfw // Initialize glfw
try glfw.init(.{}); try glfw.init(.{});