Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628const std = @import("std");const mem = std.mem;const assert = @import("../quirks.zig").inlineAssert;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;const CommaSplitter = @import("CommaSplitter.zig");
const log = std.log.scoped(.cli);
// TODO:// - Only `--long=value` format is accepted. Do we want to allow// `--long value`? Not currently allowed.
// For trimmingpub 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 "compatibility", it must be of type/// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to/// handle backwards compatibility for fields with the given name. The/// field name doesn't need to exist (so you can setup compatibility for/// removed fields). The value is a function that will be called when/// all other parsing fails for that field. If a field changes such that/// the old values would NOT error, then the caller should handle that/// downstream after parsing is done, not through this method.////// 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");
// 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 = .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| err: { // If we get an error parsing a field, then we try to fall // back to compatibility handlers if able. if (@hasDecl(T, "compatibility")) { // If we have a compatibility handler for this key, then // we call it and see if it handles the error. if (T.compatibility.get(key)) |handler| { if (handler(dst, arena_alloc, key, value)) { log.info( "compatibility handler for {s} handled error, you may be using a deprecated field: {}", .{ key, err }, ); break :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 } || Error; 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.allocPrintSentinel( arena_alloc, "unknown error {}", .{err}, 0, ), };
// 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), }); }; }}
/// The function type for a compatibility handler. The compatibility/// handler is documented in the `parse` function documentation.////// The function type should return bool if the compatibility was/// handled, and false otherwise. If false is returned then the/// naturally occurring error will continue to be processed as if/// this compatibility handler was not present.////// Compatibility handlers aren't allowed to return errors because/// they're generally only called in error cases, so we already have/// an error message to show users. If there is an error in handling/// the compatibility, then the handler should return false.pub fn CompatibilityHandler(comptime T: type) type { return *const fn ( dst: *T, alloc: Allocator, key: []const u8, value: ?[]const u8, ) bool;}
/// Convenience function to create a compatibility handler that/// renames a field from `from` to `to`.pub fn compatibilityRenamed( comptime T: type, comptime to: []const u8,) CompatibilityHandler(T) { comptime assert(@hasField(T, to));
return (struct { fn compat( dst: *T, alloc: Allocator, key: []const u8, value: ?[]const u8, ) bool { _ = key;
parseIntoField(T, alloc, dst, to, value) catch |err| { log.warn("error parsing renamed field {s}: {}", .{ to, err, });
return false; };
return true; } }).compat;}
fn formatValueRequired( comptime T: type, arena_alloc: std.mem.Allocator, key: []const u8,) std.Io.Writer.Error![:0]const u8 { var stream: std.Io.Writer.Allocating = .init(arena_alloc); const writer = &stream.writer;
try writer.print("value required", .{}); try formatValues(T, key, writer); try writer.writeByte(0);
const written = stream.written(); return written[0 .. written.len - 1 :0];}
fn formatInvalidValue( comptime T: type, arena_alloc: std.mem.Allocator, key: []const u8, value: ?[]const u8,) std.Io.Writer.Error![:0]const u8 { var stream: std.Io.Writer.Allocating = .init(arena_alloc); const writer = &stream.writer;
try writer.print("invalid value \"{?s}\"", .{value}); try formatValues(T, key, writer); try writer.writeByte(0);
const written = stream.written(); return written[0 .. written.len - 1 :0];}
fn formatValues( comptime T: type, key: []const u8, writer: *std.Io.Writer,) std.Io.Writer.Error!void { @setEvalBranchQuota(2000); 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)) { // 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, }; const fieldInfo = @typeInfo(Field); const canHaveDecls = fieldInfo == .@"struct" or fieldInfo == .@"union" or fieldInfo == .@"enum";
// 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; // Set default value if possible. if (canHaveDecls and @hasDecl(Field, "init")) { try @field(dst, field.name).init(alloc); return; } const raw = field.default_value_ptr orelse break :default; const ptr: *const field.type = @ptrCast(@alignCast(raw)); @field(dst, field.name) = ptr.*; return; }
// If we are a type that can have decls and have a parseCLI decl, // we call that and use that to set the value. if (canHaveDecls) { 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; } }
return error.InvalidField;}
pub 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_ptr = 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, null), .@"packed" => parsePackedStruct(T, v), else => @compileError("unsupported struct layout"), };}
pub fn parseAutoStruct( comptime T: type, alloc: Allocator, v: []const u8, default_: ?T,) !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 = .initEmpty();
// We split each value by "," allowing for quoting and escaping. var iter: CommaSplitter = .init(v); loop: while (try 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);
// used if we need to decode a double-quoted string. var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit();
const value = value: { const 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] == '"') { // Decode a double-quoted string as a Zig string literal. const parsed = try std.zig.string_literal.parseWrite(&buf.writer, value); if (parsed == .failure) return error.InvalidValue; break :value buf.written(); }
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)) { @field(result, field.name) = default: { // If we're given a default value then we inherit those. // Otherwise we use the default values as specified by the // struct. if (default_) |default| { break :default @field(default, field.name); } else { const default_ptr = field.default_value_ptr orelse return error.InvalidValue; const typed_ptr: *const field.type = @ptrCast(@alignCast(default_ptr)); break :default typed_ptr.*; } }; } }
return result;}
pub 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 r: std.Io.Reader = .fixed( \\a=42 \\what \\b=two );
var iter: LineIterator = .{ .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 "parse: compatibility handler" { const testing = std.testing;
var data: struct { a: bool = false, _arena: ?ArenaAllocator = null,
pub const compatibility: std.StaticStringMap( CompatibilityHandler(@This()), ) = .initComptime(&.{ .{ "a", compat }, });
fn compat( self: *@This(), alloc: Allocator, key: []const u8, value: ?[]const u8, ) bool { _ = alloc; if (std.mem.eql(u8, key, "a")) { if (value) |v| { if (mem.eql(u8, v, "yuh")) { self.a = true; return true; } } }
return false; } } = .{}; defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--a=yuh", ); defer iter.deinit(); try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expect(data._arena != null); try testing.expect(data.a);}
test "parse: compatibility renamed" { const testing = std.testing;
var data: struct { a: bool = false, b: bool = false, _arena: ?ArenaAllocator = null,
pub const compatibility: std.StaticStringMap( CompatibilityHandler(@This()), ) = .initComptime(&.{ .{ "old", compatibilityRenamed(@This(), "a") }, }); } = .{}; defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, "--old=true --b=true", ); defer iter.deinit(); try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expect(data._arena != null); try testing.expect(data.a); try testing.expect(data.b);}
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: struct with init 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 init(self: *Self, _alloc: Allocator) !void { _ = _alloc; self.* = .{ .v = "HELLO!" }; } }, } = undefined;
try parseIntoField(@TypeOf(data), alloc, &data, "a", ""); try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);}
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, e: [:0]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);
// Set sentinel string field try parseIntoField(@TypeOf(data), alloc, &data, "value", "e:hello"); try testing.expectEqualStrings("hello", data.value.e);}
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"), );}
/// 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 const LineIterator = 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: *std.Io.Reader,
/// 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 init(reader: *std.Io.Reader) Self { return .{ .r = reader }; }
pub fn next(self: *Self) ?[]const u8 { // First prime the reader. // File readers at least are initialized with a size of 0, // and this will actually prompt the reader to get the actual // size of the file, which will be used in the EOF check below. // // This will also optimize reads down the line as we're // more likely to beworking with buffered data. // // fillMore asserts that the buffer has available capacity, // so skip this if it's full. if (self.r.bufferedLen() < self.r.buffer.len) { self.r.fillMore() catch {}; }
var writer: std.Io.Writer = .fixed(self.entry[2..]);
var entry = while (self.r.seek != self.r.end) { // Reset write head writer.end = 0;
_ = self.r.streamDelimiterEnding(&writer, '\n') catch |e| { log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e }); return null; }; _ = self.r.discardDelimiterInclusive('\n') catch {};
var entry = writer.buffered(); 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; break entry; } else return null;
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]; } }
// 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 .. entry.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, } }; }};
/// 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 reader: std.Io.Reader = .fixed( \\A \\B=42 \\C \\ \\# A comment \\D \\ \\ # An indented comment \\ E \\ \\# A quoted string with whitespace \\F= "value " );
var iter: LineIterator = .init(&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 reader: std.Io.Reader = .fixed("A\n\n");
var iter: LineIterator = .init(&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 reader: std.Io.Reader = .fixed("A = B\n\n");
var iter: LineIterator = .init(&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 reader: std.Io.Reader = .fixed("A = \n\n");
var iter: LineIterator = .init(&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 reader: std.Io.Reader = .fixed("A\r\nB = C\r\n");
var iter: LineIterator = .init(&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());}
test "LineIterator with buffered reader" { const testing = std.testing; var f: std.Io.Reader = .fixed("A\nB = C\n"); var buf: [2]u8 = undefined; var r = f.limited(.unlimited, &buf); const reader = &r.interface;
var iter: LineIterator = .init(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());}
test "LineIterator with buffered and primed reader" { const testing = std.testing; var f: std.Io.Reader = .fixed("A\nB = C\n"); var buf: [2]u8 = undefined; var r = f.limited(.unlimited, &buf); const reader = &r.interface;
try reader.fill(buf.len);
var iter: LineIterator = .init(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());}