aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/args.zig
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/libghostty-vt/src/cli/args.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/args.zig1627
1 files changed, 1627 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/cli/args.zig b/vendor/libghostty-vt/src/cli/args.zig
new file mode 100644
index 00000000..bd5060d6
--- /dev/null
+++ b/vendor/libghostty-vt/src/cli/args.zig
@@ -0,0 +1,1627 @@
+const 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 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 "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());
+}