diff --git a/src/cli_args.zig b/src/cli_args.zig index 76bcbe245..55fc2136c 100644 --- a/src/cli_args.zig +++ b/src/cli_args.zig @@ -4,10 +4,20 @@ const assert = std.debug.assert; const Allocator = mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const ErrorList = @import("config/ErrorList.zig"); + // TODO: // - Only `--long=value` format is accepted. Do we want to allow // `--long value`? Not currently allowed. +/// The base errors for arg parsing. Additional errors can be returned due +/// to type-specific parsing but these are always possible. +pub const Error = error{ + ValueRequired, + InvalidField, + InvalidValue, +}; + /// Parse the command line arguments from iter into dst. /// /// dst must be a struct. The fields and their types will be used to determine @@ -19,6 +29,10 @@ const ArenaAllocator = std.heap.ArenaAllocator; /// an arena allocator will be created (or reused if set already) for any /// allocations. Allocations are necessary for certain types, like `[]const u8`. /// +/// If the destination type has a field "_errors" of type "ErrorList" then +/// errors will be added to that list. In this case, the only error returned by +/// parse are allocation errors. +/// /// Note: If the arena is already non-null, then it will be used. In this /// case, in the case of an error some memory might be leaked into the arena. pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void { @@ -62,11 +76,45 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void { break :value null; }; - try parseIntoField(T, arena_alloc, dst, key, value); + parseIntoField(T, arena_alloc, dst, key, value) catch |err| { + if (comptime !canTrackErrors(T)) return err; + switch (err) { + error.InvalidField => try dst._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "unknown field: {s}", + .{key}, + ), + }), + + error.ValueRequired => try dst._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "{s}: value required", + .{key}, + ), + }), + + error.InvalidValue => try dst._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "{s}: invalid value", + .{key}, + ), + }), + + else => return err, + } + }; } } } +/// Returns true if this type can track errors. +fn canTrackErrors(comptime T: type) bool { + return @hasField(T, "_errors"); +} + /// Parse a single key/value pair into the destination type T. /// /// This may result in allocations. The allocations can only be freed by freeing @@ -240,6 +288,29 @@ test "parse: quoted value" { try testing.expectEqualStrings("hello!", data.b); } +test "parse: error tracking" { + const testing = std.testing; + + var data: struct { + a: []const u8 = "", + b: enum { one } = .one, + + _arena: ?ArenaAllocator = null, + _errors: ErrorList = .{}, + } = .{}; + defer if (data._arena) |arena| arena.deinit(); + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + testing.allocator, + "--what --a=42", + ); + defer iter.deinit(); + try parse(@TypeOf(data), testing.allocator, &data, &iter); + try testing.expect(data._arena != null); + try testing.expectEqualStrings("42", data.a); + try testing.expect(!data._errors.empty()); +} + test "parseIntoField: ignore underscore-prefixed fields" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); diff --git a/src/config/Config.zig b/src/config/Config.zig index c37939d22..c2d2f4f8d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -14,6 +14,7 @@ const cli_args = @import("../cli_args.zig"); const Key = @import("key.zig").Key; const KeyValue = @import("key.zig").Value; +const ErrorList = @import("ErrorList.zig"); const log = std.log.scoped(.config); @@ -341,6 +342,11 @@ keybind: Keybinds = .{}, /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, +/// List of errors that occurred while loading. This can be accessed directly +/// by callers. It is only underscore-prefixed so it can't be set by the +/// configuration file. +_errors: ErrorList = .{}, + pub fn deinit(self: *Config) void { if (self._arena) |arena| arena.deinit(); self.* = undefined; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 9cbd2f1c4..81d22327b 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -393,7 +393,7 @@ pub const Set = struct { alloc: Allocator, t: Trigger, action: Action, - ) !void { + ) Allocator.Error!void { // unbind should never go into the set, it should be handled prior assert(action != .unbind);