cli: suggest the closest enum value on a typo

A misspelt value for an enum-typed flag now names the nearest tag as
did_you_mean. --dump's peeking validator treats a word within two
edits of a format name as that mistake rather than as the url, so
--dump htmx no longer becomes a second url.

tagNames moves from Config into cli so both can use it.
This commit is contained in:
Adrià Arrufat committed 2026-09-08 17:33:48 +02:00
1 parent 88d133a676
commit a32d30fa65
2 files changed
+24 -15

No files matched your search

+7 -7
View File
@@ -21,6 +21,7 @@ const zenai = @import("zenai");
const lp = @import("lightpanda");
const cli = @import("cli.zig");
const string = @import("string.zig");
const dump = @import("browser/dump.zig");
const Mime = @import("browser/Mime.zig");
@@ -316,6 +317,11 @@ fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?DumpF
var peek_args = args.*;
if (peek_args.next()) |next_arg| {
const mode = std.meta.stringToEnum(DumpFormat, next_arg) orelse {
// Anything else is the positional url, unless it is a misspelt format.
if (string.closest(next_arg, tagNames(DumpFormat), 2)) |near| {
log.fatal(.app, "invalid option choice", .{ .arg = "--dump", .value = log.red(next_arg), .did_you_mean = log.green(near) });
return error.InvalidArgument;
}
target.* = .html;
return;
};
@@ -1371,13 +1377,7 @@ pub fn validateUserAgent(ua: []const u8) !void {
/// Tag names of a Zig enum, so a command's allowed values can't drift from the
/// enum it sets.
pub fn tagNames(comptime E: type) []const []const u8 {
const fields = @typeInfo(E).@"enum".fields;
var names: [fields.len][]const u8 = undefined;
for (fields, &names) |f, *n| n.* = f.name;
const frozen = names;
return &frozen;
}
pub const tagNames = cli.tagNames;
/// `<a|b|c>` ghost-text hint built from the same enum's tag names.
pub fn tagHint(comptime E: type) []const u8 {
+17 -8
View File
@@ -190,6 +190,16 @@ const string = @import("string.zig");
/// .help => |tag| printHelp(tag),
/// }
/// ```
pub fn tagNames(comptime E: type) []const []const u8 {
return comptime blk: {
const fields = @typeInfo(E).@"enum".fields;
var names: [fields.len][]const u8 = undefined;
for (fields, &names) |f, *n| n.* = f.name;
const frozen = names;
break :blk &frozen;
};
}
pub fn Builder(comptime commands: anytype) type {
return struct {
const Self = @This();
@@ -211,13 +221,7 @@ pub fn Builder(comptime commands: anytype) type {
break :blk @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, len));
};
const command_names: []const []const u8 = blk: {
var names: []const []const u8 = &.{};
for (std.meta.fieldNames(Enum)) |name| {
names = names ++ &[_][]const u8{name};
}
break :blk names;
};
const command_names = tagNames(Enum);
/// Creates an array of `StructField` out of given options.
fn optionsToStructFields(comptime options: anytype) [options.len]std.builtin.Type.StructField {
@@ -701,7 +705,12 @@ pub fn Builder(comptime commands: anytype) type {
const str = args.next() orelse return error.MissingArgument;
const v = std.meta.stringToEnum(E, str) orelse {
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = str });
const value = log.red(str);
if (string.closest(str, tagNames(E), 2)) |near| {
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = value, .did_you_mean = log.green(near) });
} else {
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = value });
}
return error.InvalidArgument;
};