mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-09 11:26:41 +00:00
cli: move cli_args.zig to cli
This commit is contained in:
678
src/cli/args.zig
Normal file
678
src/cli/args.zig
Normal file
@@ -0,0 +1,678 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const ErrorList = @import("../config/ErrorList.zig");
|
||||
|
||||
// TODO:
|
||||
// - Only `--long=value` format is accepted. Do we want to allow
|
||||
// `--long value`? Not currently allowed.
|
||||
|
||||
/// 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 "_errors" of type "ErrorList" then
|
||||
/// errors will be added to that list. In this case, the only error returned by
|
||||
/// parse are allocation errors.
|
||||
///
|
||||
/// 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 = ArenaAllocator.init(alloc);
|
||||
arena_owned = true;
|
||||
}
|
||||
|
||||
break :arena dst._arena.?.allocator();
|
||||
} else fail: {
|
||||
// Note: this is... not safe...
|
||||
var fail = std.testing.FailingAllocator.init(alloc, 0);
|
||||
break :fail fail.allocator();
|
||||
};
|
||||
errdefer if (arena_available and arena_owned) {
|
||||
dst._arena.?.deinit();
|
||||
dst._arena = null;
|
||||
};
|
||||
|
||||
while (iter.next()) |arg| {
|
||||
if (mem.startsWith(u8, arg, "--")) {
|
||||
var key: []const u8 = arg[2..];
|
||||
const value: ?[]const u8 = value: {
|
||||
// If the arg has "=" then the value is after the "=".
|
||||
if (mem.indexOf(u8, key, "=")) |idx| {
|
||||
defer key = key[0..idx];
|
||||
break :value key[idx + 1 ..];
|
||||
}
|
||||
|
||||
break :value null;
|
||||
};
|
||||
|
||||
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
|
||||
if (comptime !canTrackErrors(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};
|
||||
switch (@as(ErrSet, @errSetCast(err))) {
|
||||
// OOM is not recoverable since we need to allocate to
|
||||
// track more error messages.
|
||||
error.OutOfMemory => return err,
|
||||
|
||||
error.InvalidField => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: unknown field",
|
||||
.{key},
|
||||
),
|
||||
}),
|
||||
|
||||
error.ValueRequired => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: value required",
|
||||
.{key},
|
||||
),
|
||||
}),
|
||||
|
||||
error.InvalidValue => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: invalid value",
|
||||
.{key},
|
||||
),
|
||||
}),
|
||||
|
||||
else => try dst._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"{s}: unknown error {}",
|
||||
.{ key, err },
|
||||
),
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this type can track errors.
|
||||
fn canTrackErrors(comptime T: type) bool {
|
||||
return @hasField(T, "_errors");
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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,
|
||||
};
|
||||
|
||||
// If we are a struct and have parseCLI, we call that and use
|
||||
// that to set the value.
|
||||
switch (@typeInfo(Field)) {
|
||||
.Struct => 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 => try @field(dst, field.name).parseCLI(value),
|
||||
|
||||
// 3 arg = (self, alloc, input) => void
|
||||
3 => try @field(dst, field.name).parseCLI(alloc, value),
|
||||
|
||||
// 4 arg = (self, alloc, errors, input) => void
|
||||
4 => if (comptime canTrackErrors(T)) {
|
||||
try @field(dst, field.name).parseCLI(alloc, &dst._errors, value);
|
||||
} else {
|
||||
var list: ErrorList = .{};
|
||||
try @field(dst, field.name).parseCLI(alloc, &list, value);
|
||||
if (!list.empty()) return error.InvalidValue;
|
||||
},
|
||||
|
||||
else => @compileError("parseCLI invalid argument count"),
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
|
||||
.Enum => {
|
||||
@field(dst, field.name) = std.meta.stringToEnum(
|
||||
Field,
|
||||
value orelse return error.ValueRequired,
|
||||
) orelse return error.InvalidValue;
|
||||
return;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// 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);
|
||||
mem.copy(u8, 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);
|
||||
mem.copy(u8, buf, slice);
|
||||
buf[slice.len] = 0;
|
||||
break :value buf;
|
||||
},
|
||||
|
||||
bool => try parseBool(value orelse "t"),
|
||||
|
||||
u8 => std.fmt.parseInt(
|
||||
u8,
|
||||
value orelse return error.ValueRequired,
|
||||
0,
|
||||
) catch return error.InvalidValue,
|
||||
|
||||
u32 => std.fmt.parseInt(
|
||||
u32,
|
||||
value orelse return error.ValueRequired,
|
||||
0,
|
||||
) catch return error.InvalidValue,
|
||||
|
||||
f64 => std.fmt.parseFloat(
|
||||
f64,
|
||||
value orelse return error.ValueRequired,
|
||||
) catch return error.InvalidValue,
|
||||
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return error.InvalidField;
|
||||
}
|
||||
|
||||
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: error tracking" {
|
||||
const testing = std.testing;
|
||||
|
||||
var data: struct {
|
||||
a: []const u8 = "",
|
||||
b: enum { one } = .one,
|
||||
|
||||
_arena: ?ArenaAllocator = null,
|
||||
_errors: ErrorList = .{},
|
||||
} = .{};
|
||||
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._errors.empty());
|
||||
}
|
||||
|
||||
test "parseIntoField: ignore underscore-prefixed fields" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
_a: []const u8 = "12",
|
||||
} = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidField,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "_a", "42"),
|
||||
);
|
||||
try testing.expectEqualStrings("12", data._a);
|
||||
}
|
||||
|
||||
test "parseIntoField: string" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: []const u8,
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
|
||||
try testing.expectEqualStrings("42", data.a);
|
||||
}
|
||||
|
||||
test "parseIntoField: sentinel string" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: [:0]const u8,
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
|
||||
try testing.expectEqualStrings("42", data.a);
|
||||
try testing.expectEqual(@as(u8, 0), data.a[data.a.len]);
|
||||
}
|
||||
|
||||
test "parseIntoField: bool" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: bool,
|
||||
} = undefined;
|
||||
|
||||
// True
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "1");
|
||||
try testing.expectEqual(true, data.a);
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "t");
|
||||
try testing.expectEqual(true, data.a);
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "T");
|
||||
try testing.expectEqual(true, data.a);
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "true");
|
||||
try testing.expectEqual(true, data.a);
|
||||
|
||||
// False
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "0");
|
||||
try testing.expectEqual(false, data.a);
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "f");
|
||||
try testing.expectEqual(false, data.a);
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "F");
|
||||
try testing.expectEqual(false, data.a);
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "false");
|
||||
try testing.expectEqual(false, data.a);
|
||||
}
|
||||
|
||||
test "parseIntoField: unsigned numbers" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
u8: u8,
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "u8", "1");
|
||||
try testing.expectEqual(@as(u8, 1), data.u8);
|
||||
}
|
||||
|
||||
test "parseIntoField: floats" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
f64: f64,
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "f64", "1");
|
||||
try testing.expectEqual(@as(f64, 1.0), data.f64);
|
||||
}
|
||||
|
||||
test "parseIntoField: enums" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const Enum = enum { one, two, three };
|
||||
var data: struct {
|
||||
v: Enum,
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "v", "two");
|
||||
try testing.expectEqual(Enum.two, data.v);
|
||||
}
|
||||
|
||||
test "parseIntoField: 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.?);
|
||||
}
|
||||
|
||||
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: struct with parse func with error tracking" {
|
||||
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();
|
||||
|
||||
pub fn parseCLI(
|
||||
_: Self,
|
||||
parse_alloc: Allocator,
|
||||
errors: *ErrorList,
|
||||
value: ?[]const u8,
|
||||
) !void {
|
||||
_ = value;
|
||||
try errors.add(parse_alloc, .{ .message = "OH NO!" });
|
||||
}
|
||||
} = .{},
|
||||
|
||||
_errors: ErrorList = .{},
|
||||
} = .{};
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
|
||||
try testing.expect(!data._errors.empty());
|
||||
}
|
||||
|
||||
test "parseIntoField: struct with parse func with unsupported error tracking" {
|
||||
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();
|
||||
|
||||
pub fn parseCLI(
|
||||
_: Self,
|
||||
parse_alloc: Allocator,
|
||||
errors: *ErrorList,
|
||||
value: ?[]const u8,
|
||||
) !void {
|
||||
_ = value;
|
||||
try errors.add(parse_alloc, .{ .message = "OH NO!" });
|
||||
}
|
||||
} = .{},
|
||||
} = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "a", "42"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns an iterator (implements "next") that reads CLI args by line.
|
||||
/// Each CLI arg is expected to be a single line. This is used to implement
|
||||
/// configuration files.
|
||||
pub fn LineIterator(comptime ReaderType: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// The maximum size a single line can be. We don't expect any
|
||||
/// CLI arg to exceed this size. Can't wait to git blame this in
|
||||
/// like 4 years and be wrong about this.
|
||||
pub const MAX_LINE_SIZE = 4096;
|
||||
|
||||
r: ReaderType,
|
||||
entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)),
|
||||
|
||||
pub fn next(self: *Self) ?[]const u8 {
|
||||
// TODO: detect "--" prefixed lines and give a friendlier error
|
||||
const buf = buf: {
|
||||
while (true) {
|
||||
// Read the full line
|
||||
var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch {
|
||||
// TODO: handle errors
|
||||
unreachable;
|
||||
} orelse return null;
|
||||
|
||||
// Trim any whitespace around it
|
||||
const whitespace = " \t";
|
||||
const trim = std.mem.trim(u8, entry, whitespace);
|
||||
if (trim.len != entry.len) {
|
||||
std.mem.copy(u8, entry, trim);
|
||||
entry = entry[0..trim.len];
|
||||
}
|
||||
|
||||
// Ignore blank lines and comments
|
||||
if (entry.len == 0 or entry[0] == '#') continue;
|
||||
|
||||
// Trim spaces around '='
|
||||
if (mem.indexOf(u8, entry, "=")) |idx| {
|
||||
const key = std.mem.trim(u8, entry[0..idx], whitespace);
|
||||
const value = value: {
|
||||
var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace);
|
||||
|
||||
// Detect a quoted string.
|
||||
if (value.len >= 2 and
|
||||
value[0] == '"' and
|
||||
value[value.len - 1] == '"')
|
||||
{
|
||||
// Trim quotes since our CLI args processor expects
|
||||
// quotes to already be gone.
|
||||
value = value[1 .. value.len - 1];
|
||||
}
|
||||
|
||||
break :value value;
|
||||
};
|
||||
|
||||
const len = key.len + value.len + 1;
|
||||
if (entry.len != len) {
|
||||
std.mem.copy(u8, entry, key);
|
||||
entry[key.len] = '=';
|
||||
std.mem.copy(u8, entry[key.len + 1 ..], value);
|
||||
entry = entry[0..len];
|
||||
}
|
||||
}
|
||||
|
||||
break :buf entry;
|
||||
}
|
||||
};
|
||||
|
||||
// We need to reslice so that we include our '--' at the beginning
|
||||
// of our buffer so that we can trick the CLI parser to treat it
|
||||
// as CLI args.
|
||||
return self.entry[0 .. buf.len + 2];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Constructs a LineIterator (see docs for that).
|
||||
pub fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) {
|
||||
return .{ .r = reader };
|
||||
}
|
||||
|
||||
test "LineIterator" {
|
||||
const testing = std.testing;
|
||||
var fbs = std.io.fixedBufferStream(
|
||||
\\A
|
||||
\\B=42
|
||||
\\C
|
||||
\\
|
||||
\\# A comment
|
||||
\\D
|
||||
\\
|
||||
\\ # An indented comment
|
||||
\\ E
|
||||
\\
|
||||
\\# A quoted string with whitespace
|
||||
\\F= "value "
|
||||
);
|
||||
|
||||
var iter = lineIterator(fbs.reader());
|
||||
try testing.expectEqualStrings("--A", iter.next().?);
|
||||
try testing.expectEqualStrings("--B=42", iter.next().?);
|
||||
try testing.expectEqualStrings("--C", iter.next().?);
|
||||
try testing.expectEqualStrings("--D", iter.next().?);
|
||||
try testing.expectEqualStrings("--E", iter.next().?);
|
||||
try testing.expectEqualStrings("--F=value ", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
test "LineIterator end in newline" {
|
||||
const testing = std.testing;
|
||||
var fbs = std.io.fixedBufferStream("A\n\n");
|
||||
|
||||
var iter = lineIterator(fbs.reader());
|
||||
try testing.expectEqualStrings("--A", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
test "LineIterator spaces around '='" {
|
||||
const testing = std.testing;
|
||||
var fbs = std.io.fixedBufferStream("A = B\n\n");
|
||||
|
||||
var iter = lineIterator(fbs.reader());
|
||||
try testing.expectEqualStrings("--A=B", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
Reference in New Issue
Block a user