From 497f0363e5009c835818292fce223dbf38f68eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Sat, 26 Sep 2026 23:33:30 +0200 Subject: [PATCH] cli: share the "invalid option choice" error `cli.invalidChoice` logs a bad value with its closest match, and the generic enum path, --dump, --log-level, --log-format and now --log-filter all use it. editDistance lowercases its inputs once, and closest strips exactly a leading `--`, as its doc says. --- src/Config.zig | 25 +++++-------------------- src/cli.zig | 20 +++++++++++--------- src/string.zig | 20 ++++++++++++-------- 3 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index 4f8fc0cb3..e1e83e1c6 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -69,8 +69,7 @@ fn logFilterValidator(allocator: Allocator, args: *std.process.Args.Iterator, li } const v = std.meta.stringToEnum(log.Scope, name) orelse { - log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter", .value = part }); - return error.InvalidOption; + return cli.invalidChoice("--log-filter", name, comptime tagNames(log.Scope) ++ &[_][]const u8{"all"}); }; try list.append(allocator, .{ .scope = v, .enable = enable }); @@ -85,9 +84,7 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l return; } - target.* = std.meta.stringToEnum(log.Level, str) orelse { - return invalidChoice("--log-level", str, comptime tagNames(log.Level) ++ &[_][]const u8{"error"}); - }; + target.* = std.meta.stringToEnum(log.Level, str) orelse return cli.invalidChoice("--log-level", str, tagNames(log.Level)); log.opts.level = target.*.?; } @@ -98,20 +95,9 @@ fn mcpLogDefaults() void { log.opts.format = .logfmt; } -fn invalidChoice(arg: []const u8, value: []const u8, choices: []const []const u8) error{InvalidArgument} { - if (string.closest(value, choices)) |near| { - log.fatal(.app, "invalid option choice", .{ .arg = arg, .value = log.red(value), .did_you_mean = log.green(near) }); - } else { - log.fatal(.app, "invalid option choice", .{ .arg = arg, .value = log.red(value) }); - } - return error.InvalidArgument; -} - fn logFormatValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?log.Format) !void { const str = args.next() orelse return error.MissingArgument; - const format = std.meta.stringToEnum(log.Format, str) orelse { - return invalidChoice("--log-format", str, tagNames(log.Format)); - }; + const format = std.meta.stringToEnum(log.Format, str) orelse return cli.invalidChoice("--log-format", str, tagNames(log.Format)); target.* = format; log.opts.format = format; } @@ -330,9 +316,8 @@ fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?DumpF 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))) |near| { - log.fatal(.app, "invalid option choice", .{ .arg = "--dump", .value = log.red(next_arg), .did_you_mean = log.green(near) }); - return error.InvalidArgument; + if (string.closest(next_arg, tagNames(DumpFormat)) != null) { + return cli.invalidChoice("--dump", next_arg, tagNames(DumpFormat)); } target.* = .html; return; diff --git a/src/cli.zig b/src/cli.zig index 7bb869ea8..06dbe5cf2 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -200,6 +200,16 @@ pub fn tagNames(comptime E: type) []const []const u8 { }; } +/// Logs a bad value for `arg` with the closest of `choices`, if any. +pub fn invalidChoice(arg: []const u8, value: []const u8, choices: []const []const u8) error{InvalidArgument} { + if (string.closest(value, choices)) |near| { + log.fatal(.app, "invalid option choice", .{ .arg = arg, .value = log.red(value), .did_you_mean = log.green(near) }); + } else { + log.fatal(.app, "invalid option choice", .{ .arg = arg, .value = log.red(value) }); + } + return error.InvalidArgument; +} + pub fn Builder(comptime commands: anytype) type { return struct { const Self = @This(); @@ -688,15 +698,7 @@ pub fn Builder(comptime commands: anytype) type { }; const str = args.next() orelse return error.MissingArgument; - const v = std.meta.stringToEnum(E, str) orelse { - const value = log.red(str); - if (string.closest(str, tagNames(E))) |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; - }; + const v = std.meta.stringToEnum(E, str) orelse return invalidChoice(kebab_cased, str, tagNames(E)); if (is_multiple) { try target.append(allocator, v); diff --git a/src/string.zig b/src/string.zig index 9fa507f62..4931eca35 100644 --- a/src/string.zig +++ b/src/string.zig @@ -422,18 +422,21 @@ pub fn isOneOf(needle: []const u8, haystack: []const []const u8) bool { fn editDistance(a: []const u8, b: []const u8) usize { const max = 64; if (a.len > max or b.len > max) return std.math.maxInt(usize); + var a_buf: [max]u8 = undefined; + var b_buf: [max]u8 = undefined; + const la = std.ascii.lowerString(&a_buf, a); + const lb = std.ascii.lowerString(&b_buf, b); + var prev2: [max + 1]u8 = undefined; var prev: [max + 1]u8 = undefined; var cur: [max + 1]u8 = undefined; - for (0..b.len + 1) |j| prev[j] = @intCast(j); - for (a, 1..) |ca, i| { - const la = std.ascii.toLower(ca); + for (0..lb.len + 1) |j| prev[j] = @intCast(j); + for (la, 1..) |ca, i| { cur[0] = @intCast(i); - for (b, 1..) |cb, j| { - const lb = std.ascii.toLower(cb); - const cost: u8 = if (la == lb) 0 else 1; + for (lb, 1..) |cb, j| { + const cost: u8 = if (ca == cb) 0 else 1; cur[j] = @min(@min(prev[j] + 1, cur[j - 1] + 1), prev[j - 1] + cost); - if (i > 1 and j > 1 and la == std.ascii.toLower(b[j - 2]) and std.ascii.toLower(a[i - 2]) == lb) { + if (i > 1 and j > 1 and ca == lb[j - 2] and la[i - 2] == cb) { cur[j] = @min(cur[j], prev2[j - 2] + 1); } } @@ -447,7 +450,8 @@ fn editDistance(a: []const u8, b: []const u8) usize { /// (rustc's rule). A leading `--` doesn't count toward the length, since every /// flag shares it. Earlier candidates win ties. pub fn closest(name: []const u8, candidates: []const []const u8) ?[]const u8 { - const max_dist = @max(std.mem.trimStart(u8, name, "-").len, 3) / 3; + const typed = if (std.mem.startsWith(u8, name, "--")) name[2..] else name; + const max_dist = @max(typed.len, 3) / 3; var best: ?[]const u8 = null; var best_dist: usize = std.math.maxInt(usize); for (candidates) |cand| {