cli: positional arguments are invalid when parsing configuration

This commit is contained in:
Mitchell Hashimoto
2024-10-17 21:39:34 -07:00
parent 4e25840e08
commit 940a46d41f
2 changed files with 87 additions and 37 deletions

View File

@ -93,46 +93,60 @@ pub fn parse(
} }
} }
if (mem.startsWith(u8, arg, "--")) { // If this doesn't start with "--" then it isn't a config
var key: []const u8 = arg[2..]; // flag. We don't support positional arguments or configuration
const value: ?[]const u8 = value: { // values set with spaces so this is an error.
// If the arg has "=" then the value is after the "=". if (!mem.startsWith(u8, arg, "--")) {
if (mem.indexOf(u8, key, "=")) |idx| { if (comptime !canTrackDiags(T)) return Error.InvalidField;
defer key = key[0..idx];
break :value key[idx + 1 ..];
}
break :value null; // Add our diagnostic
}; try dst._diagnostics.append(arena_alloc, .{
.key = try arena_alloc.dupeZ(u8, arg),
.message = "invalid field",
.location = diags.Location.fromIter(iter),
});
parseIntoField(T, arena_alloc, dst, key, value) catch |err| { continue;
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};
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 => "value required",
error.InvalidValue => "invalid value",
else => try std.fmt.allocPrintZ(
arena_alloc,
"unknown error {}",
.{err},
),
};
// Add our diagnostic
try dst._diagnostics.append(arena_alloc, .{
.key = try arena_alloc.dupeZ(u8, key),
.message = message,
.location = diags.Location.fromIter(iter),
});
};
} }
var key: []const u8 = arg[2..];
const value: ?[]const u8 = value: {
// If the arg has "=" then the value is after the "=".
if (mem.indexOf(u8, key, "=")) |idx| {
defer key = key[0..idx];
break :value key[idx + 1 ..];
}
break :value null;
};
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
if (comptime !canTrackDiags(T)) return err;
// The error set is dependent on comptime T, so we always add
// an extra error so we can have the "else" below.
const ErrSet = @TypeOf(err) || error{Unknown};
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 => "value required",
error.InvalidValue => "invalid value",
else => try std.fmt.allocPrintZ(
arena_alloc,
"unknown error {}",
.{err},
),
};
// Add our diagnostic
try dst._diagnostics.append(arena_alloc, .{
.key = try arena_alloc.dupeZ(u8, key),
.message = message,
.location = diags.Location.fromIter(iter),
});
};
} }
} }
@ -452,6 +466,27 @@ test "parse: empty value resets to default" {
try testing.expect(!data.b); 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" { test "parse: diagnostic tracking" {
const testing = std.testing; const testing = std.testing;

View File

@ -46,6 +46,8 @@ pub const Location = union(enum) {
line: usize, line: usize,
}, },
pub const Key = @typeInfo(Location).Union.tag_type.?;
pub fn fromIter(iter: anytype) Location { pub fn fromIter(iter: anytype) Location {
const Iter = t: { const Iter = t: {
const T = @TypeOf(iter); const T = @TypeOf(iter);
@ -121,4 +123,17 @@ pub const DiagnosticList = struct {
pub fn items(self: *const DiagnosticList) []const Diagnostic { pub fn items(self: *const DiagnosticList) []const Diagnostic {
return self.list.items; return self.list.items;
} }
/// Returns true if there are any diagnostics for the given
/// location type.
pub fn containsLocation(
self: *const DiagnosticList,
location: Location.Key,
) bool {
for (self.list.items) |diag| {
if (diag.location == location) return true;
}
return false;
}
}; };