const std = @import("std"); const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const diags = @import("diagnostics.zig"); const internal_os = @import("../os/main.zig"); const Diagnostic = diags.Diagnostic; const DiagnosticList = diags.DiagnosticList; // TODO: // - Only `--long=value` format is accepted. Do we want to allow // `--long value`? Not currently allowed. // For trimming pub const whitespace = " \t"; /// 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 /// 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 /// to do custom parsing. /// /// If the destination type has a field "_arena" of type `?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 "_diagnostics", it must be of type /// "DiagnosticList" and any diagnostic messages will be added to that list. /// When diagnostics are present, only allocation errors will be returned. /// /// If the destination type has a decl "renamed", it must be of type /// std.StaticStringMap([]const u8) and contains a mapping from the old /// field name to the new field name. This is used to allow renaming fields /// while still supporting the old name. If a renamed field is set, parsing /// will automatically set the new field name. /// /// 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 { const info = @typeInfo(T); assert(info == .Struct); comptime { // Verify all renamed fields are valid (source does not exist, // destination does exist). if (@hasDecl(T, "renamed")) { for (T.renamed.keys(), T.renamed.values()) |key, value| { if (@hasField(T, key)) { @compileLog(key); @compileError("renamed field source exists"); } if (!@hasField(T, value)) { @compileLog(value); @compileError("renamed field destination does not exist"); } } } } // Make an arena for all our allocations if we support it. Otherwise, // use an allocator that always fails. If the arena is already set on // the config, then we reuse that. See memory note in parse docs. const arena_available = @hasField(T, "_arena"); var arena_owned: bool = false; const arena_alloc = if (arena_available) arena: { // If the arena is unset, we create it. We mark that we own it // only so that we can clean it up on error. if (dst._arena == null) { dst._arena = ArenaAllocator.init(alloc); arena_owned = true; } break :arena dst._arena.?.allocator(); } else fail: { // Note: this is... not safe... var fail = std.testing.FailingAllocator.init(alloc, .{}); break :fail fail.allocator(); }; errdefer if (arena_available and arena_owned) { dst._arena.?.deinit(); dst._arena = null; }; while (iter.next()) |arg| { // Do manual parsing if we have a hook for it. if (@hasDecl(T, "parseManuallyHook")) { if (!try dst.parseManuallyHook( arena_alloc, arg, iter, )) return; } // If the destination supports help then we check for it, call // the help function and return. if (@hasDecl(T, "help")) { if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { try dst.help(); return; } } // If this doesn't start with "--" then it isn't a config // flag. We don't support positional arguments or configuration // values set with spaces so this is an error. if (!mem.startsWith(u8, arg, "--")) { if (comptime !canTrackDiags(T)) return Error.InvalidField; // Add our diagnostic try dst._diagnostics.append(arena_alloc, .{ .key = try arena_alloc.dupeZ(u8, arg), .message = "invalid field", .location = try diags.Location.fromIter(iter, arena_alloc), }); continue; } var key: []const u8 = arg[2..]; const value: ?[]const u8 = value: { // If the arg has "=" then the value is after the "=". if (mem.indexOf(u8, key, "=")) |idx| { defer key = key[0..idx]; break :value key[idx + 1 ..]; } break :value null; }; parseIntoField(T, arena_alloc, dst, key, value) catch |err| { if (comptime !canTrackDiags(T)) return err; // The error set is dependent on comptime T, so we always add // an extra error so we can have the "else" below. const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory }; const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) { // OOM is not recoverable since we need to allocate to // track more error messages. error.OutOfMemory => return err, error.InvalidField => "unknown field", error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required", error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value", else => try std.fmt.allocPrintZ( arena_alloc, "unknown error {}", .{err}, ), }; // Add our diagnostic try dst._diagnostics.append(arena_alloc, .{ .key = try arena_alloc.dupeZ(u8, key), .message = message, .location = try diags.Location.fromIter(iter, arena_alloc), }); }; } } fn formatValueRequired( comptime T: type, arena_alloc: std.mem.Allocator, key: []const u8, ) std.mem.Allocator.Error![:0]const u8 { var buf = std.ArrayList(u8).init(arena_alloc); errdefer buf.deinit(); const writer = buf.writer(); try writer.print("value required", .{}); try formatValues(T, key, writer); try writer.writeByte(0); return buf.items[0 .. buf.items.len - 1 :0]; } fn formatInvalidValue( comptime T: type, arena_alloc: std.mem.Allocator, key: []const u8, value: ?[]const u8, ) std.mem.Allocator.Error![:0]const u8 { var buf = std.ArrayList(u8).init(arena_alloc); errdefer buf.deinit(); const writer = buf.writer(); try writer.print("invalid value \"{?s}\"", .{value}); try formatValues(T, key, writer); try writer.writeByte(0); return buf.items[0 .. buf.items.len - 1 :0]; } fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void { const typeinfo = @typeInfo(T); inline for (typeinfo.Struct.fields) |f| { if (std.mem.eql(u8, key, f.name)) { switch (@typeInfo(f.type)) { .Enum => |e| { try writer.print(", valid values are: ", .{}); inline for (e.fields, 0..) |field, i| { if (i != 0) try writer.print(", ", .{}); try writer.print("{s}", .{field.name}); } }, else => {}, } break; } } } /// Returns true if this type can track diagnostics. fn canTrackDiags(comptime T: type) bool { return @hasField(T, "_diagnostics"); } /// Parse a single key/value pair into the destination type T. /// /// 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. pub fn parseIntoField( comptime T: type, alloc: Allocator, dst: *T, key: []const u8, value: ?[]const u8, ) !void { const info = @typeInfo(T); assert(info == .Struct); inline for (info.Struct.fields) |field| { if (field.name[0] != '_' and mem.eql(u8, field.name, key)) { // If the value is empty string (set but empty string), // then we reset the value to the default. if (value) |v| default: { if (v.len != 0) break :default; const raw = field.default_value orelse break :default; const ptr: *const field.type = @alignCast(@ptrCast(raw)); @field(dst, field.name) = ptr.*; return; } // 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.type)) { .Optional => |opt| opt.child, else => field.type, }; // If we are a type that can have decls and have a parseCLI decl, // we call that and use that to set the value. const fieldInfo = @typeInfo(Field); if (fieldInfo == .Struct or fieldInfo == .Union or fieldInfo == .Enum) { if (@hasDecl(Field, "parseCLI")) { const fnInfo = @typeInfo(@TypeOf(Field.parseCLI)).Fn; switch (fnInfo.params.len) { // 1 arg = (input) => output 1 => @field(dst, field.name) = try Field.parseCLI(value), // 2 arg = (self, input) => void 2 => switch (@typeInfo(field.type)) { .Struct, .Union, .Enum, => try @field(dst, field.name).parseCLI(value), // If the field is optional and set, then we use // the pointer value directly into it. If its not // set we need to create a new instance. .Optional => if (@field(dst, field.name)) |*v| { try v.parseCLI(value); } else { // Note: you cannot do @field(dst, name) = undefined // because this causes the value to be "null" // in ReleaseFast modes. var tmp: Field = undefined; try tmp.parseCLI(value); @field(dst, field.name) = tmp; }, else => @compileError("unexpected field type"), }, // 3 arg = (self, alloc, input) => void 3 => switch (@typeInfo(field.type)) { .Struct, .Union, .Enum, => try @field(dst, field.name).parseCLI(alloc, value), .Optional => if (@field(dst, field.name)) |*v| { try v.parseCLI(alloc, value); } else { var tmp: Field = undefined; try tmp.parseCLI(alloc, value); @field(dst, field.name) = tmp; }, else => @compileError("unexpected field type"), }, else => @compileError("parseCLI invalid argument count"), } return; } } // No parseCLI, magic the value based on the type @field(dst, field.name) = switch (Field) { []const u8 => value: { const slice = value orelse return error.ValueRequired; const buf = try alloc.alloc(u8, slice.len); @memcpy(buf, slice); break :value buf; }, [:0]const u8 => value: { const slice = value orelse return error.ValueRequired; const buf = try alloc.allocSentinel(u8, slice.len, 0); @memcpy(buf, slice); buf[slice.len] = 0; break :value buf; }, bool => try parseBool(value orelse "t"), inline u8, u16, u21, u32, u64, usize, i8, i16, i32, i64, isize, => |Int| std.fmt.parseInt( Int, value orelse return error.ValueRequired, 0, ) catch return error.InvalidValue, f32, f64, => |Float| std.fmt.parseFloat( Float, value orelse return error.ValueRequired, ) catch return error.InvalidValue, else => switch (fieldInfo) { .Enum => std.meta.stringToEnum( Field, value orelse return error.ValueRequired, ) orelse return error.InvalidValue, .Struct => try parseStruct( Field, alloc, value orelse return error.ValueRequired, ), .Union => try parseTaggedUnion( Field, alloc, value orelse return error.ValueRequired, ), else => @compileError("unsupported field type"), }, }; return; } } // Unknown field, is the field renamed? if (@hasDecl(T, "renamed")) { for (T.renamed.keys(), T.renamed.values()) |old, new| { if (mem.eql(u8, old, key)) { try parseIntoField(T, alloc, dst, new, value); return; } } } return error.InvalidField; } fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T { const info = @typeInfo(T).Union; assert(@typeInfo(info.tag_type.?) == .Enum); // Get the union tag that is being set. We support values with no colon // if the value is void so its not an error to have no colon. const colon_idx = mem.indexOf(u8, v, ":") orelse v.len; const tag_str = std.mem.trim(u8, v[0..colon_idx], whitespace); const value = if (colon_idx < v.len) v[colon_idx + 1 ..] else ""; // Find the field in the union that matches the tag. inline for (info.fields) |field| { if (mem.eql(u8, field.name, tag_str)) { // Special case void types where we don't need a value. if (field.type == void) { if (value.len > 0) return error.InvalidValue; return @unionInit(T, field.name, {}); } // We need to create a struct that looks like this union field. // This lets us use parseIntoField as if its a dedicated struct. const Target = @Type(.{ .Struct = .{ .layout = .auto, .fields = &.{.{ .name = field.name, .type = field.type, .default_value = null, .is_comptime = false, .alignment = @alignOf(field.type), }}, .decls = &.{}, .is_tuple = false, } }); // Parse the value into the struct var t: Target = undefined; try parseIntoField(Target, alloc, &t, field.name, value); // Build our union return @unionInit(T, field.name, @field(t, field.name)); } } return error.InvalidValue; } fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { return switch (@typeInfo(T).Struct.layout) { .auto => parseAutoStruct(T, alloc, v), .@"packed" => parsePackedStruct(T, v), else => @compileError("unsupported struct layout"), }; } pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { const info = @typeInfo(T).Struct; comptime assert(info.layout == .auto); // We start our result as undefined so we don't get an error for required // fields. We track required fields below and we validate that we set them // all at the bottom of this function (in addition to setting defaults for // optionals). var result: T = undefined; // Keep track of which fields were set so we can error if a required // field was not set. const FieldSet = std.StaticBitSet(info.fields.len); var fields_set: FieldSet = FieldSet.initEmpty(); // We split each value by "," var iter = std.mem.splitSequence(u8, v, ","); loop: while (iter.next()) |entry| { // Find the key/value, trimming whitespace. The value may be quoted // which we strip the quotes from. const idx = mem.indexOf(u8, entry, ":") orelse return error.InvalidValue; const key = std.mem.trim(u8, entry[0..idx], whitespace); const value = value: { var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); // Detect a quoted string. if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') { // Trim quotes since our CLI args processor expects // quotes to already be gone. value = value[1 .. value.len - 1]; } break :value value; }; inline for (info.fields, 0..) |field, i| { if (std.mem.eql(u8, field.name, key)) { try parseIntoField(T, alloc, &result, key, value); fields_set.set(i); continue :loop; } } // No field matched return error.InvalidValue; } // Ensure all required fields are set inline for (info.fields, 0..) |field, i| { if (!fields_set.isSet(i)) { const default_ptr = field.default_value orelse return error.InvalidValue; const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); @field(result, field.name) = typed_ptr.*; } } return result; } fn parsePackedStruct(comptime T: type, v: []const u8) !T { const info = @typeInfo(T).Struct; comptime assert(info.layout == .@"packed"); var result: T = .{}; // Allow standalone boolean values like "true" and "false" to // turn on or off all of the struct's fields. bools: { const b = parseBool(v) catch break :bools; inline for (info.fields) |field| { assert(field.type == bool); @field(result, field.name) = b; } return result; } // We split each value by "," var iter = std.mem.splitSequence(u8, v, ","); loop: while (iter.next()) |part_raw| { // Determine the field we're looking for and the value. If the // field is prefixed with "no-" then we set the value to false. const part, const value = part: { const negation_prefix = "no-"; const trimmed = std.mem.trim(u8, part_raw, whitespace); if (std.mem.startsWith(u8, trimmed, negation_prefix)) { break :part .{ trimmed[negation_prefix.len..], false }; } else { break :part .{ trimmed, true }; } }; inline for (info.fields) |field| { assert(field.type == bool); if (std.mem.eql(u8, field.name, part)) { @field(result, field.name) = value; continue :loop; } } // No field matched return error.InvalidValue; } return result; } pub fn parseBool(v: []const u8) !bool { const t = &[_][]const u8{ "1", "t", "T", "true" }; const f = &[_][]const u8{ "0", "f", "F", "false" }; inline for (t) |str| { if (mem.eql(u8, v, str)) return true; } inline for (f) |str| { if (mem.eql(u8, v, str)) return false; } return error.InvalidValue; } test "parse: simple" { const testing = std.testing; var data: struct { a: []const u8 = "", b: bool = false, @"b-f": bool = true, _arena: ?ArenaAllocator = null, } = .{}; defer if (data._arena) |arena| arena.deinit(); var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--a=42 --b --b-f=false", ); 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.b); try testing.expect(!data.@"b-f"); // Reparsing works var iter2 = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--a=84", ); defer iter2.deinit(); try parse(@TypeOf(data), testing.allocator, &data, &iter2); try testing.expect(data._arena != null); try testing.expectEqualStrings("84", data.a); try testing.expect(data.b); try testing.expect(!data.@"b-f"); } test "parse: quoted value" { const testing = std.testing; var data: struct { a: u8 = 0, b: []const u8 = "", _arena: ?ArenaAllocator = null, } = .{}; defer if (data._arena) |arena| arena.deinit(); var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--a=\"42\" --b=\"hello!\"", ); defer iter.deinit(); try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expectEqual(@as(u8, 42), data.a); try testing.expectEqualStrings("hello!", data.b); } test "parse: empty value resets to default" { const testing = std.testing; var data: struct { a: u8 = 42, b: bool = false, _arena: ?ArenaAllocator = null, } = .{}; defer if (data._arena) |arena| arena.deinit(); var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--a= --b=", ); defer iter.deinit(); try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expectEqual(@as(u8, 42), data.a); try testing.expect(!data.b); } test "parse: positional arguments are invalid" { const testing = std.testing; var data: struct { a: u8 = 42, _arena: ?ArenaAllocator = null, } = .{}; defer if (data._arena) |arena| arena.deinit(); var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--a=84 what", ); defer iter.deinit(); try testing.expectError( error.InvalidField, parse(@TypeOf(data), testing.allocator, &data, &iter), ); try testing.expectEqual(@as(u8, 84), data.a); } test "parse: diagnostic tracking" { const testing = std.testing; var data: struct { a: []const u8 = "", b: enum { one } = .one, _arena: ?ArenaAllocator = null, _diagnostics: DiagnosticList = .{}, } = .{}; 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._diagnostics.items().len == 1); { const diag = data._diagnostics.items()[0]; try testing.expectEqual(diags.Location.none, diag.location); try testing.expectEqualStrings("what", diag.key); try testing.expectEqualStrings("unknown field", diag.message); } } test "parse: diagnostic location" { const testing = std.testing; var data: struct { a: []const u8 = "", b: enum { one, two } = .one, _arena: ?ArenaAllocator = null, _diagnostics: DiagnosticList = .{}, } = .{}; defer if (data._arena) |arena| arena.deinit(); var fbs = std.io.fixedBufferStream( \\a=42 \\what \\b=two ); const r = fbs.reader(); const Iter = LineIterator(@TypeOf(r)); var iter: Iter = .{ .r = r, .filepath = "test" }; try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expect(data._arena != null); try testing.expectEqualStrings("42", data.a); try testing.expect(data.b == .two); try testing.expect(data._diagnostics.items().len == 1); { const diag = data._diagnostics.items()[0]; try testing.expectEqualStrings("what", diag.key); try testing.expectEqualStrings("unknown field", diag.message); try testing.expectEqualStrings("test", diag.location.file.path); try testing.expectEqual(2, diag.location.file.line); } } test "parseIntoField: ignore underscore-prefixed fields" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { _a: []const u8 = "12", } = .{}; try testing.expectError( error.InvalidField, parseIntoField(@TypeOf(data), alloc, &data, "_a", "42"), ); try testing.expectEqualStrings("12", data._a); } test "parseIntoField: string" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: []const u8, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "a", "42"); try testing.expectEqualStrings("42", data.a); } test "parseIntoField: sentinel string" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: [:0]const u8, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "a", "42"); try testing.expectEqualStrings("42", data.a); try testing.expectEqual(@as(u8, 0), data.a[data.a.len]); } test "parseIntoField: bool" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: bool, } = undefined; // True try parseIntoField(@TypeOf(data), alloc, &data, "a", "1"); try testing.expectEqual(true, data.a); try parseIntoField(@TypeOf(data), alloc, &data, "a", "t"); try testing.expectEqual(true, data.a); try parseIntoField(@TypeOf(data), alloc, &data, "a", "T"); try testing.expectEqual(true, data.a); try parseIntoField(@TypeOf(data), alloc, &data, "a", "true"); try testing.expectEqual(true, data.a); // False try parseIntoField(@TypeOf(data), alloc, &data, "a", "0"); try testing.expectEqual(false, data.a); try parseIntoField(@TypeOf(data), alloc, &data, "a", "f"); try testing.expectEqual(false, data.a); try parseIntoField(@TypeOf(data), alloc, &data, "a", "F"); try testing.expectEqual(false, data.a); try parseIntoField(@TypeOf(data), alloc, &data, "a", "false"); try testing.expectEqual(false, data.a); } test "parseIntoField: unsigned numbers" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { u8: u8, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "u8", "1"); try testing.expectEqual(@as(u8, 1), data.u8); } test "parseIntoField: floats" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { f64: f64, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "f64", "1"); try testing.expectEqual(@as(f64, 1.0), data.f64); } test "parseIntoField: enums" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const Enum = enum { one, two, three }; var data: struct { v: Enum, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "v", "two"); try testing.expectEqual(Enum.two, data.v); } test "parseIntoField: packed struct" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const Field = packed struct { a: bool = false, b: bool = true, }; var data: struct { v: Field, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "v", "b"); try testing.expect(!data.v.a); try testing.expect(data.v.b); } test "parseIntoField: packed struct negation" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const Field = packed struct { a: bool = false, b: bool = true, }; var data: struct { v: Field, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "v", "a,no-b"); try testing.expect(data.v.a); try testing.expect(!data.v.b); } test "parseIntoField: packed struct true/false" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const Field = packed struct { a: bool = false, b: bool = true, }; var data: struct { v: Field, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "v", "true"); try testing.expect(data.v.a); try testing.expect(data.v.b); try parseIntoField(@TypeOf(data), alloc, &data, "v", "false"); try testing.expect(!data.v.a); try testing.expect(!data.v.b); try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "v", "true,a"), ); } test "parseIntoField: packed struct whitespace" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const Field = packed struct { a: bool = false, b: bool = true, }; var data: struct { v: Field, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "v", " a, no-b "); try testing.expect(data.v.a); try testing.expect(!data.v.b); } test "parseIntoField: optional field" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: ?bool = null, } = .{}; // True try parseIntoField(@TypeOf(data), alloc, &data, "a", "1"); try testing.expectEqual(true, data.a.?); // Unset try parseIntoField(@TypeOf(data), alloc, &data, "a", ""); try testing.expect(data.a == null); } test "parseIntoField: struct with parse func" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: struct { const Self = @This(); v: []const u8, pub fn parseCLI(value: ?[]const u8) !Self { _ = value; return Self{ .v = "HELLO!" }; } }, } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "a", "42"); try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v); } test "parseIntoField: optional struct with parse func" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: ?struct { const Self = @This(); v: []const u8, pub fn parseCLI(self: *Self, _: Allocator, value: ?[]const u8) !void { _ = value; self.* = .{ .v = "HELLO!" }; } } = null, } = .{}; try parseIntoField(@TypeOf(data), alloc, &data, "a", "42"); try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.?.v); } test "parseIntoField: struct with basic fields" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { value: struct { a: []const u8, b: u32, c: u8 = 12, } = undefined, } = .{}; // Set required fields try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello,b:42"); try testing.expectEqualStrings("hello", data.value.a); try testing.expectEqual(42, data.value.b); try testing.expectEqual(12, data.value.c); // Set all fields try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:world,b:84,c:24"); try testing.expectEqualStrings("world", data.value.a); try testing.expectEqual(84, data.value.b); try testing.expectEqual(24, data.value.c); // Missing require dfield try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), ); } test "parseIntoField: tagged union" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { value: union(enum) { a: u8, b: u8, c: void, d: []const u8, } = undefined, } = .{}; // Set one field try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:1"); try testing.expectEqual(1, data.value.a); // Set another try parseIntoField(@TypeOf(data), alloc, &data, "value", "b:2"); try testing.expectEqual(2, data.value.b); // Set void field try parseIntoField(@TypeOf(data), alloc, &data, "value", "c"); try testing.expectEqual({}, data.value.c); // Set string field try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello"); try testing.expectEqualStrings("hello", data.value.d); } test "parseIntoField: tagged union unknown filed" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { value: union(enum) { a: u8, b: u8, } = undefined, } = .{}; try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "c:1"), ); } test "parseIntoField: tagged union invalid field value" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { value: union(enum) { a: u8, b: u8, } = undefined, } = .{}; try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), ); } test "parseIntoField: tagged union missing tag" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { value: union(enum) { a: u8, b: u8, } = undefined, } = .{}; try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a"), ); try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", ":a"), ); } test "parseIntoField: renamed field" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var data: struct { a: []const u8, const renamed = std.StaticStringMap([]const u8).initComptime(&.{ .{ "old", "a" }, }); } = undefined; try parseIntoField(@TypeOf(data), alloc, &data, "old", "42"); try testing.expectEqualStrings("42", data.a); } /// An iterator that considers its location to be CLI args. It /// iterates through an underlying iterator and increments a counter /// to track the current CLI arg index. /// /// This also ignores any argument that starts with `+`. It assumes that /// actions were parsed out before this iterator was created. pub fn ArgsIterator(comptime Iterator: type) type { return struct { const Self = @This(); /// The underlying args iterator. iterator: Iterator, /// Our current index into the iterator. This is 1-indexed. /// The 0 value is used to indicate that we haven't read any /// values yet. index: usize = 0, pub fn deinit(self: *Self) void { if (@hasDecl(Iterator, "deinit")) { self.iterator.deinit(); } } pub fn next(self: *Self) ?[]const u8 { const value = self.iterator.next() orelse return null; self.index += 1; // We ignore any argument that starts with "+". This is used // to indicate actions and are expected to be parsed out before // this iterator is created. if (value.len > 0 and value[0] == '+') return self.next(); return value; } /// Returns a location for a diagnostic message. pub fn location(self: *const Self, _: Allocator) error{}!?diags.Location { return .{ .cli = self.index }; } }; } /// Create an args iterator for the process args. This will skip argv0. pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError!ArgsIterator(internal_os.args.ArgIterator) { var iter = try internal_os.args.iterator(alloc_gpa); errdefer iter.deinit(); _ = iter.next(); // skip argv0 return .{ .iterator = iter }; } test "ArgsIterator" { const testing = std.testing; const child = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--what +list-things --a=42", ); const Iter = ArgsIterator(@TypeOf(child)); var iter: Iter = .{ .iterator = child }; defer iter.deinit(); try testing.expectEqualStrings("--what", iter.next().?); try testing.expectEqualStrings("--a=42", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } /// Returns an iterator (implements "next") that reads CLI args by line. /// Each CLI arg is expected to be a single line. This is used to implement /// configuration files. pub fn LineIterator(comptime ReaderType: type) type { return struct { const Self = @This(); /// The maximum size a single line can be. We don't expect any /// CLI arg to exceed this size. Can't wait to git blame this in /// like 4 years and be wrong about this. pub const MAX_LINE_SIZE = 4096; /// Our stateful reader. r: ReaderType, /// Filepath that is used for diagnostics. This is only used for /// diagnostic messages so it can be formatted however you want. /// It is prefixed to the messages followed by the line number. filepath: []const u8 = "", /// The current line that we're on. This is 1-indexed because /// lines are generally 1-indexed in the real world. The value /// can be zero if we haven't read any lines yet. line: usize = 0, /// This is the buffer where we store the current entry that /// is formatted to be compatible with the parse function. entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)), pub fn next(self: *Self) ?[]const u8 { // TODO: detect "--" prefixed lines and give a friendlier error const buf = buf: { while (true) { // Read the full line var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch { // TODO: handle errors unreachable; } orelse return null; // Increment our line counter self.line += 1; // Trim any whitespace (including CR) around it const trim = std.mem.trim(u8, entry, whitespace ++ "\r"); if (trim.len != entry.len) { std.mem.copyForwards(u8, entry, trim); entry = entry[0..trim.len]; } // Ignore blank lines and comments if (entry.len == 0 or entry[0] == '#') continue; // Trim spaces around '=' if (mem.indexOf(u8, entry, "=")) |idx| { const key = std.mem.trim(u8, entry[0..idx], whitespace); const value = value: { var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); // Detect a quoted string. if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') { // Trim quotes since our CLI args processor expects // quotes to already be gone. value = value[1 .. value.len - 1]; } break :value value; }; const len = key.len + value.len + 1; if (entry.len != len) { std.mem.copyForwards(u8, entry, key); entry[key.len] = '='; std.mem.copyForwards(u8, entry[key.len + 1 ..], value); entry = entry[0..len]; } } break :buf entry; } }; // We need to reslice so that we include our '--' at the beginning // of our buffer so that we can trick the CLI parser to treat it // as CLI args. return self.entry[0 .. buf.len + 2]; } /// Returns a location for a diagnostic message. pub fn location( self: *const Self, alloc: Allocator, ) Allocator.Error!?diags.Location { // If we have no filepath then we have no location. if (self.filepath.len == 0) return null; return .{ .file = .{ .path = try alloc.dupe(u8, self.filepath), .line = self.line, } }; } }; } // Constructs a LineIterator (see docs for that). fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) { return .{ .r = reader }; } /// An iterator valid for arg parsing from a slice. pub const SliceIterator = struct { const Self = @This(); slice: []const []const u8, idx: usize = 0, pub fn next(self: *Self) ?[]const u8 { if (self.idx >= self.slice.len) return null; defer self.idx += 1; return self.slice[self.idx]; } }; /// Construct a SliceIterator from a slice. pub fn sliceIterator(slice: []const []const u8) SliceIterator { return .{ .slice = slice }; } test "LineIterator" { const testing = std.testing; var fbs = std.io.fixedBufferStream( \\A \\B=42 \\C \\ \\# A comment \\D \\ \\ # An indented comment \\ E \\ \\# A quoted string with whitespace \\F= "value " ); var iter = lineIterator(fbs.reader()); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=42", iter.next().?); try testing.expectEqualStrings("--C", iter.next().?); try testing.expectEqualStrings("--D", iter.next().?); try testing.expectEqualStrings("--E", iter.next().?); try testing.expectEqualStrings("--F=value ", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } test "LineIterator end in newline" { const testing = std.testing; var fbs = std.io.fixedBufferStream("A\n\n"); var iter = lineIterator(fbs.reader()); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } test "LineIterator spaces around '='" { const testing = std.testing; var fbs = std.io.fixedBufferStream("A = B\n\n"); var iter = lineIterator(fbs.reader()); try testing.expectEqualStrings("--A=B", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } test "LineIterator no value" { const testing = std.testing; var fbs = std.io.fixedBufferStream("A = \n\n"); var iter = lineIterator(fbs.reader()); try testing.expectEqualStrings("--A=", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } test "LineIterator with CRLF line endings" { const testing = std.testing; var fbs = std.io.fixedBufferStream("A\r\nB = C\r\n"); var iter = lineIterator(fbs.reader()); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=C", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); }