diff options
Diffstat (limited to 'src/cli/args.zig')
| -rw-r--r-- | src/cli/args.zig | 271 |
1 files changed, 135 insertions, 136 deletions
diff --git a/src/cli/args.zig b/src/cli/args.zig index c4a40acf5..a34560b78 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -162,10 +162,11 @@ pub fn parse( error.InvalidField => "unknown field", error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required", error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value", - else => try std.fmt.allocPrintZ( + else => try std.fmt.allocPrintSentinel( arena_alloc, "unknown error {}", .{err}, + 0, ), }; @@ -235,14 +236,16 @@ fn formatValueRequired( comptime T: type, arena_alloc: std.mem.Allocator, key: []const u8, -) std.mem.Allocator.Error![:0]const u8 { - var buf = std.ArrayList(u8).init(arena_alloc); - errdefer buf.deinit(); - const writer = buf.writer(); +) 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); - return buf.items[0 .. buf.items.len - 1 :0]; + + const written = stream.written(); + return written[0 .. written.len - 1 :0]; } fn formatInvalidValue( @@ -250,17 +253,23 @@ fn formatInvalidValue( arena_alloc: std.mem.Allocator, key: []const u8, value: ?[]const u8, -) std.mem.Allocator.Error![:0]const u8 { - var buf = std.ArrayList(u8).init(arena_alloc); - errdefer buf.deinit(); - const writer = buf.writer(); +) 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); - return buf.items[0 .. buf.items.len - 1 :0]; + + const written = stream.written(); + return written[0 .. written.len - 1 :0]; } -fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void { +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| { @@ -542,8 +551,8 @@ pub fn parseAutoStruct( const key = std.mem.trim(u8, entry[0..idx], whitespace); // used if we need to decode a double-quoted string. - var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); const value = value: { const value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); @@ -554,10 +563,9 @@ pub fn parseAutoStruct( value[value.len - 1] == '"') { // Decode a double-quoted string as a Zig string literal. - const writer = buf.writer(alloc); - const parsed = try std.zig.string_literal.parseWrite(writer, value); + const parsed = try std.zig.string_literal.parseWrite(&buf.writer, value); if (parsed == .failure) return error.InvalidValue; - break :value buf.items; + break :value buf.written(); } break :value value; @@ -795,15 +803,13 @@ test "parse: diagnostic location" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var fbs = std.io.fixedBufferStream( + var r: std.Io.Reader = .fixed( \\a=42 \\what \\b=two ); - const r = fbs.reader(); - const Iter = LineIterator(@TypeOf(r)); - var iter: Iter = .{ .r = r, .filepath = "test" }; + 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); @@ -1208,18 +1214,7 @@ test "parseIntoField: struct with basic fields" { try testing.expectEqual(84, data.value.b); try testing.expectEqual(24, data.value.c); - // Set with explicit default - data.value = try parseAutoStruct( - @TypeOf(data.value), - alloc, - "a:hello", - .{ .a = "oh no", .b = 42 }, - ); - try testing.expectEqualStrings("hello", data.value.a); - try testing.expectEqual(42, data.value.b); - try testing.expectEqual(12, data.value.c); - - // Missing required field + // Missing require dfield try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), @@ -1395,115 +1390,119 @@ test "ArgsIterator" { /// Returns an iterator (implements "next") that reads CLI args by line. /// Each CLI arg is expected to be a single line. This is used to implement /// configuration files. -pub fn LineIterator(comptime ReaderType: type) type { - return struct { - const Self = @This(); +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; + /// The maximum size a single line can be. We don't expect any + /// CLI arg to exceed this size. Can't wait to git blame this in + /// like 4 years and be wrong about this. + pub const MAX_LINE_SIZE = 4096; - /// Our stateful reader. - r: ReaderType, + /// 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 = "", + /// 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, + /// 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)), + /// This is the buffer where we store the current entry that + /// is formatted to be compatible with the parse function. + entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)), - pub fn next(self: *Self) ?[]const u8 { - // TODO: detect "--" prefixed lines and give a friendlier error - const buf = buf: { - while (true) { - // Read the full line - var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch |err| switch (err) { - inline else => |e| { - log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e }); - return null; - }, - } orelse return null; + pub fn init(reader: *std.Io.Reader) Self { + return .{ .r = reader }; + } - // Increment our line counter - self.line += 1; + 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. + 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 {}; - // 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]; - } + var entry = writer.buffered(); + self.line += 1; - // Ignore blank lines and comments - if (entry.len == 0 or entry[0] == '#') continue; - - // Trim spaces around '=' - if (mem.indexOf(u8, entry, "=")) |idx| { - const key = std.mem.trim(u8, entry[0..idx], whitespace); - const value = value: { - var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); - - // Detect a quoted string. - if (value.len >= 2 and - value[0] == '"' and - value[value.len - 1] == '"') - { - // Trim quotes since our CLI args processor expects - // quotes to already be gone. - value = value[1 .. value.len - 1]; - } - - break :value value; - }; - - const len = key.len + value.len + 1; - if (entry.len != len) { - std.mem.copyForwards(u8, entry, key); - entry[key.len] = '='; - std.mem.copyForwards(u8, entry[key.len + 1 ..], value); - entry = entry[0..len]; - } - } + // 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]; + } - break :buf entry; + // 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; }; - // We need to reslice so that we include our '--' at the beginning - // of our buffer so that we can trick the CLI parser to treat it - // as CLI args. - return self.entry[0 .. buf.len + 2]; + 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]; + } } - /// 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, - } }; - } - }; -} + // 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]; + } -// Constructs a LineIterator (see docs for that). -fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) { - return .{ .r = reader }; -} + /// 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 { @@ -1526,7 +1525,7 @@ pub fn sliceIterator(slice: []const []const u8) SliceIterator { test "LineIterator" { const testing = std.testing; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\A \\B=42 \\C @@ -1541,7 +1540,7 @@ test "LineIterator" { \\F= "value " ); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=42", iter.next().?); try testing.expectEqualStrings("--C", iter.next().?); @@ -1554,9 +1553,9 @@ test "LineIterator" { test "LineIterator end in newline" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A\n\n"); + var reader: std.Io.Reader = .fixed("A\n\n"); - var iter = lineIterator(fbs.reader()); + 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()); @@ -1564,9 +1563,9 @@ test "LineIterator end in newline" { test "LineIterator spaces around '='" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A = B\n\n"); + var reader: std.Io.Reader = .fixed("A = B\n\n"); - var iter = lineIterator(fbs.reader()); + 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()); @@ -1574,18 +1573,18 @@ test "LineIterator spaces around '='" { test "LineIterator no value" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A = \n\n"); + var reader: std.Io.Reader = .fixed("A = \n\n"); - var iter = lineIterator(fbs.reader()); + 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 fbs = std.io.fixedBufferStream("A\r\nB = C\r\n"); + var reader: std.Io.Reader = .fixed("A\r\nB = C\r\n"); - var iter = lineIterator(fbs.reader()); + 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()); |
