diff --git a/src/Config.zig b/src/Config.zig index 43e9289f..85db1920 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -51,7 +51,7 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.ArgIterator var it = std.mem.splitScalar(u8, str, ','); while (it.next()) |part| { const v = std.meta.stringToEnum(log.Scope, part) orelse { - log.fatal(.app, "invalid option choice", .{ .arg = "log-filter-scopes", .value = part }); + log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter-scopes", .value = part }); return error.InvalidOption; }; @@ -106,6 +106,18 @@ fn dumpValidator(_: Allocator, args: *std.process.ArgIterator) !?DumpFormat { return .html; } +fn waitScriptFileValidator(allocator: Allocator, args: *std.process.ArgIterator) !?[:0]const u8 { + const path = args.next() orelse { + log.fatal(.app, "missing argument value", .{ .arg = "--wait-script-file" }); + return error.InvalidArgument; + }; + + return std.fs.cwd().readFileAllocOptions(allocator, path, 1024 * 1024, null, .of(u8), 0) catch |err| { + log.fatal(.app, "failed to read file", .{ .arg = "--wait-script-file", .path = path, .err = err }); + return error.InvalidArgument; + }; +} + /// Definition for all the commands and its arguments. See @cli.zig for further. const Commands = cli.Builder(.{ .{ @@ -131,7 +143,13 @@ const Commands = cli.Builder(.{ .{ .name = "strip_mode", .type = dump.Opts.Strip, .default = dump.Opts.Strip{} }, .{ .name = "wait_ms", .type = u32, .default = 5_000 }, .{ .name = "wait_until", .type = ?WaitUntil }, - .{ .name = "wait_script", .type = ?[:0]const u8 }, + .{ + .name = "wait_script", + .type = ?[:0]const u8, + .variants = .{ + .{ .name = "wait_script_file", .validator = waitScriptFileValidator }, + }, + }, .{ .name = "wait_selector", .type = ?[:0]const u8 }, .{ .name = "terminate_ms", .type = ?u32 }, }, diff --git a/src/cli.zig b/src/cli.zig index a7d8f955..bf5c35aa 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -339,6 +339,198 @@ pub fn Builder(comptime commands: anytype) type { return error.UnknownCommand; } + /// Returns the type for validator function. + pub fn ValidatorFn(comptime T: type, comptime is_multiple: bool) type { + if (is_multiple) { + return *const fn (Allocator, *std.process.ArgIterator, *std.ArrayList(T)) anyerror!void; + } + + return *const fn (Allocator, *std.process.ArgIterator) anyerror!T; + } + + /// Turns a snake_case string to kebab-case in comptime. + fn toKebabCase(comptime str: []const u8) [str.len]u8 { + var output: [str.len]u8 = str[0..str.len].*; + for (&output) |*c| if (c.* == '_') { + c.* = '-'; + }; + return output; + } + + fn parseValue( + allocator: Allocator, + args: *std.process.ArgIterator, + /// Pointer to field; *T. + target: anytype, + /// `Option` doesn't have a concrete type; this field expects: + /// ```zig + /// Option{ + /// .name = "option_name", + /// .type = T, + /// .multiple = ?bool, + /// .validator = ?ValidatorFn(T, is_multiple), + /// }; + /// ``` + option: anytype, + ) !void { + const kebab_cased = "--" ++ comptime toKebabCase(option.name); + + const OptionType = @TypeOf(option); + const is_multiple = @hasField(OptionType, "multiple") and option.multiple; + const has_validator = @hasField(OptionType, "validator"); + + // Prefer validator for parsing if provided. + if (has_validator) { + const validator = option.validator; + if (is_multiple) { + // Pass the list. + try @call(.auto, validator, .{ allocator, args, target }); + } else { + // Receive the value from return. + const v = try @call(.auto, validator, .{ allocator, args }); + target.* = v; + } + + return; + } + + // Extract type info. + const T = option.type; + const option_info = blk: { + const info = @typeInfo(T); + // If wrapped in optional, prefer the child type. + if (info == .optional) break :blk @typeInfo(info.optional.child); + break :blk info; + }; + + // Parse by type. + return switch (option_info) { + .int => |int| { + const Int = std.meta.Int(int.signedness, int.bits); + + const str = args.next() orelse return error.MissingArgument; + const v = std.fmt.parseInt(Int, str, 10) catch |err| { + switch (err) { + error.Overflow => log.fatal(.app, "range overflow", .{ .arg = kebab_cased, .value = str }), + error.InvalidCharacter => log.fatal(.app, "invalid character", .{ .arg = kebab_cased, .value = str }), + } + return error.InvalidArgument; + }; + + if (is_multiple) { + // Push to ArrayList. + try target.append(allocator, v); + } else { + target.* = v; + } + }, + .pointer => |pointer| { + const not_u8_slice = pointer.child != u8 or pointer.size != .slice; + if (not_u8_slice) { + @compileError("Only []u8, []const u8, [:sentinel]u8 and [:sentinel]const u8 pointers are supported"); + } + + const v = blk: { + const str = args.next() orelse return error.MissingArgument; + + // DupeZ branch. + if (comptime pointer.sentinel()) |sentinel| { + const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len + 1); + @memcpy(buf[0..str.len], str); + buf[str.len] = sentinel; + break :blk buf[0..str.len :sentinel]; + } + + // Dupe branch. + const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len); + @memcpy(buf, str); + break :blk buf; + }; + + if (is_multiple) { + try target.append(allocator, v); + } else { + target.* = v; + } + }, + .@"struct" => |_struct| { + // Don't support multiple for structs for now. + if (is_multiple) { + @compileError("multiple option is not supported for structs"); + } + + const not_packed = _struct.layout != .@"packed"; + if (not_packed) { + @compileError("only packed structs are allowed"); + } + + const str = args.next() orelse return error.MissingArgument; + + if (std.mem.eql(u8, str, "full")) { + // "full" sets all the fields of packed struct. + const Int = _struct.backing_integer orelse @compileError("packed struct must provide a backing integer"); + target.* = @bitCast(@as(Int, std.math.maxInt(Int))); + } else { + // Parse given args. + var it = std.mem.tokenizeScalar(u8, str, ','); + outer: while (it.next()) |part| { + const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace); + + inline for (_struct.fields) |f| { + lp.assert(f.type == bool, "all fields of packed struct must be boolean", .{ + .option = option.name, + .field = f.name, + }); + + if (std.mem.eql(u8, trimmed, @as([]const u8, f.name))) { + @field(target, f.name) = true; + continue :outer; + } + } + + // Invalid option choice. + log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = trimmed }); + return error.InvalidArgument; + } + } + }, + .@"enum" => { + const E = switch (@typeInfo(T)) { + .optional => |optional| optional.child, + inline else => T, + }; + + const str = args.next() orelse return error.MissingArgument; + const v = std.meta.stringToEnum(E, str) orelse { + log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = str }); + return error.InvalidArgument; + }; + + if (is_multiple) { + try target.append(allocator, v); + } else { + target.* = v; + } + }, + .bool => { + if (is_multiple) { + @compileError("multiple option is not supported for booleans"); + } + + const default = blk: { + if (@hasField(@TypeOf(option), "default")) { + break :blk option.default; + } + break :blk false; + }; + + // Set opposite of the default. + target.* = !default; + }, + else => unreachable, + }; + } + /// Parses the command with its options. fn parseCommand( allocator: Allocator, @@ -359,171 +551,39 @@ pub fn Builder(comptime commands: anytype) type { inline for (options) |option| { // We allow both `--my-option` and `--my_option` variants; // assuming given `option` struct prefer snake_case for `name`. - const kebab_cased = comptime casing: { - var output: [option.name.len]u8 = undefined; - @memcpy(&output, option.name); - std.mem.replaceScalar(u8, &output, '_', '-'); - break :casing "--" ++ output; - }; - // Match an option. - const match = - std.mem.eql(u8, option_name, "--" ++ option.name) or - std.mem.eql(u8, option_name, kebab_cased); + if (std.mem.eql(u8, option_name, "--" ++ option.name) or + std.mem.eql(u8, option_name, "--" ++ comptime toKebabCase(option.name))) + { + try parseValue(allocator, args, &@field(c, option.name), option); + continue :iter_args; + } - if (match) { - const T = option.type; - const option_info = blk: { - const info = @typeInfo(T); - // If wrapped in optional, prefer the child type. - if (info == .optional) break :blk @typeInfo(info.optional.child); - break :blk info; - }; - - const is_multiple = @hasField(@TypeOf(option), "multiple") and option.multiple; - const has_validator = @hasField(@TypeOf(option), "validator"); - - // Prefer custom validator logic instead. - if (has_validator) { - const validator = option.validator; - if (is_multiple) { - // Pass the list. - try @call(.auto, validator, .{ allocator, args, &@field(c, option.name) }); - } else { - // Receive the value from return. - const v = try @call(.auto, validator, .{ allocator, args }); - @field(c, option.name) = v; - } - } else { - switch (option_info) { - .int => |int| { - const Int = std.meta.Int(int.signedness, int.bits); - - const str = args.next() orelse return error.MissingArgument; - const v = std.fmt.parseInt(Int, str, 10) catch |err| { - switch (err) { - error.Overflow => log.fatal(.app, "range overflow", .{ .arg = kebab_cased, .value = str }), - error.InvalidCharacter => log.fatal(.app, "invalid character", .{ .arg = kebab_cased, .value = str }), - } - return error.InvalidArgument; - }; - - if (is_multiple) { - // Push to ArrayList. - try @field(c, option.name).append(allocator, v); - } else { - @field(c, option.name) = v; - } - }, - .pointer => |pointer| { - const not_u8_slice = pointer.child != u8 or pointer.size != .slice; - if (not_u8_slice) { - @compileError("Only []u8, []const u8, [:sentinel]u8 and [:sentinel]const u8 pointers are supported"); + const is_multiple = @hasField(@TypeOf(option), "multiple") and option.multiple; + // Parse for variants if there are. + const has_variants = @hasField(@TypeOf(option), "variants"); + if (has_variants) { + inline for (option.variants) |variant| { + if (std.mem.eql(u8, option_name, "--" ++ variant.name) or + std.mem.eql(u8, option_name, "--" ++ comptime toKebabCase(variant.name))) + { + const opts = blk: { + if (@hasField(@TypeOf(variant), "validator")) { + break :blk .{ + .name = variant.name, + .type = option.type, + .multiple = is_multiple, + .validator = variant.validator, + }; } - const v = blk: { - const str = args.next() orelse return error.MissingArgument; + break :blk .{ .name = variant.name, .type = option.type, .multiple = is_multiple }; + }; - // DupeZ branch. - if (comptime pointer.sentinel()) |sentinel| { - const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len + 1); - @memcpy(buf[0..str.len], str); - buf[str.len] = sentinel; - break :blk buf[0..str.len :sentinel]; - } - - // Dupe branch. - const buf = try allocator.alignedAlloc(u8, .fromByteUnits(pointer.alignment), str.len); - @memcpy(buf, str); - break :blk buf; - }; - - if (is_multiple) { - try @field(c, option.name).append(allocator, v); - } else { - @field(c, option.name) = v; - } - }, - .@"struct" => |_struct| { - // Don't support multiple for structs for now. - if (is_multiple) { - @compileError("multiple option is not supported for structs"); - } - - const not_packed = _struct.layout != .@"packed"; - if (not_packed) { - @compileError("only packed structs are allowed"); - } - - const str = args.next() orelse return error.MissingArgument; - - if (std.mem.eql(u8, str, "full")) { - // "full" sets all the fields of packed struct. - const Int = _struct.backing_integer.?; - @field(c, option.name) = @bitCast(@as(Int, std.math.maxInt(Int))); - } else { - // Parse given args. - var it = std.mem.tokenizeScalar(u8, str, ','); - outer: while (it.next()) |part| { - const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace); - - inline for (_struct.fields) |f| { - lp.assert(f.type == bool, "all fields of packed struct must be boolean", .{ - .option = option.name, - .field = f.name, - }); - - if (std.mem.eql(u8, trimmed, @as([]const u8, f.name))) { - @field(@field(c, option.name), f.name) = true; - continue :outer; - } - } - - // Invalid option choice. - log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = trimmed }); - return error.InvalidArgument; - } - } - }, - .@"enum" => { - const E = switch (@typeInfo(T)) { - .optional => |optional| optional.child, - inline else => T, - }; - - const str = args.next() orelse return error.MissingArgument; - const v = std.meta.stringToEnum(E, str) orelse { - log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = str }); - return error.InvalidArgument; - }; - - if (is_multiple) { - try @field(c, option.name).append(allocator, v); - } else { - @field(c, option.name) = v; - } - }, - .bool => { - if (is_multiple) { - @compileError("multiple option is not supported for booleans"); - } - - const default = blk: { - if (@hasField(@TypeOf(option), "default")) { - break :blk option.default; - } - break :blk false; - }; - - // Set opposite of the default. - @field(c, option.name) = !default; - }, - - else => {}, + try parseValue(allocator, args, &@field(c, option.name), opts); + continue :iter_args; } } - - continue :iter_args; } }