From a32d30fa65867efed4eee4506eceaa7d129a474c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 8 Sep 2026 17:33:48 +0200 Subject: [PATCH] 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. --- src/Config.zig | 14 +++++++------- src/cli.zig | 25 +++++++++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index 0bb929055..031becf6b 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -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; /// `` ghost-text hint built from the same enum's tag names. pub fn tagHint(comptime E: type) []const u8 { diff --git a/src/cli.zig b/src/cli.zig index 84987f593..916545ba3 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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; };