From 779764332acec8b44d44903e3532e95c9f9b09a2 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 2 Jan 2026 04:52:47 -0800 Subject: [PATCH] feat(cli): add `--grep` as alias for `-t`/`--test-name-pattern` in `bun test` (#25788) --- src/cli/Arguments.zig | 2 +- src/deps/zig-clap/clap.zig | 75 +++++++++++++++++++++++++++- src/deps/zig-clap/clap/comptime.zig | 10 ++++ src/deps/zig-clap/clap/streaming.zig | 4 +- 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 2a11d83320..57a2ef5986 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -218,7 +218,7 @@ pub const test_only_params = [_]ParamType{ clap.parseParam("--coverage-reporter ... Report coverage in 'text' and/or 'lcov'. Defaults to 'text'.") catch unreachable, clap.parseParam("--coverage-dir Directory for coverage files. Defaults to 'coverage'.") catch unreachable, clap.parseParam("--bail ? Exit the test suite after failures. If you do not specify a number, it defaults to 1.") catch unreachable, - clap.parseParam("-t, --test-name-pattern Run only tests with a name that matches the given regex.") catch unreachable, + clap.parseParam("-t, --test-name-pattern/--grep Run only tests with a name that matches the given regex.") catch unreachable, clap.parseParam("--reporter Test output reporter format. Available: 'junit' (requires --reporter-outfile), 'dots'. Default: console output.") catch unreachable, clap.parseParam("--reporter-outfile Output file path for the reporter format (required with --reporter).") catch unreachable, clap.parseParam("--dots Enable dots reporter. Shorthand for --reporter=dots.") catch unreachable, diff --git a/src/deps/zig-clap/clap.zig b/src/deps/zig-clap/clap.zig index 07f9ec7b79..edda626bba 100644 --- a/src/deps/zig-clap/clap.zig +++ b/src/deps/zig-clap/clap.zig @@ -8,8 +8,22 @@ pub const Names = struct { /// '-' prefix short: ?u8 = null, - /// '--' prefix + /// '--' prefix (primary name, used for display/help) long: ?[]const u8 = null, + + /// Additional '--' prefixed aliases (e.g., --grep as alias for --test-name-pattern) + long_aliases: []const []const u8 = &.{}, + + /// Check if the given name matches the primary long name or any alias + pub fn matchesLong(self: Names, name: []const u8) bool { + if (self.long) |l| { + if (mem.eql(u8, name, l)) return true; + } + for (self.long_aliases) |alias| { + if (mem.eql(u8, name, alias)) return true; + } + return false; + } }; /// Whether a param takes no value (a flag), one value, or can be specified multiple times. @@ -51,6 +65,7 @@ pub fn Param(comptime Id: type) type { /// Takes a string and parses it to a Param(Help). /// This is the reverse of 'help' but for at single parameter only. +/// Supports multiple long name variants separated by '/' (e.g., "--test-name-pattern/--grep"). pub fn parseParam(line: []const u8) !Param(Help) { @setEvalBranchQuota(999999); @@ -89,11 +104,67 @@ pub fn parseParam(line: []const u8) !Param(Help) { } else null; var res = parseParamRest(it.rest()); - res.names.long = param_str[2..]; res.names.short = short_name; + + // Parse long names - supports multiple variants separated by '/' + // e.g., "--test-name-pattern/--grep" becomes primary "test-name-pattern" with alias "grep" + const long_names = parseLongNames(param_str); + res.names.long = long_names.long; + res.names.long_aliases = long_names.long_aliases; return res; } +fn parseLongNames(comptime param_str: []const u8) Names { + comptime { + // Count how many long name variants we have (separated by '/') + var alias_count: usize = 0; + for (param_str) |c| { + if (c == '/') alias_count += 1; + } + + if (alias_count == 0) { + // No aliases, just the primary name + if (mem.startsWith(u8, param_str, "--")) { + return .{ .long = param_str[2..], .long_aliases = &.{} }; + } + return .{ .long = null, .long_aliases = &.{} }; + } + + // Parse multiple long names at comptime + // First pass: find the primary name + var primary: ?[]const u8 = null; + var name_it = mem.splitScalar(u8, param_str, '/'); + while (name_it.next()) |name_part| { + if (!mem.startsWith(u8, name_part, "--")) continue; + primary = name_part[2..]; + break; + } + + // Second pass: collect aliases into a comptime-known array type + const aliases = blk: { + var result: [alias_count][]const u8 = undefined; + var idx: usize = 0; + var it = mem.splitScalar(u8, param_str, '/'); + var is_first = true; + while (it.next()) |name_part| { + if (!mem.startsWith(u8, name_part, "--")) continue; + if (is_first) { + is_first = false; + continue; // Skip primary + } + result[idx] = name_part[2..]; + idx += 1; + } + break :blk result; + }; + + return .{ + .long = primary, + .long_aliases = &aliases, + }; + } +} + fn parseParamRest(line: []const u8) Param(Help) { if (mem.startsWith(u8, line, "<")) blk: { const len = mem.indexOfScalar(u8, line, '>') orelse break :blk; diff --git a/src/deps/zig-clap/clap/comptime.zig b/src/deps/zig-clap/clap/comptime.zig index aadeb66fd2..9c454b9d51 100644 --- a/src/deps/zig-clap/clap/comptime.zig +++ b/src/deps/zig-clap/clap/comptime.zig @@ -156,6 +156,11 @@ pub fn ComptimeClap( if (mem.eql(u8, name, "--" ++ l)) return true; } + // Check aliases + for (param.names.long_aliases) |alias| { + if (mem.eql(u8, name, "--" ++ alias)) + return true; + } } return false; @@ -173,6 +178,11 @@ pub fn ComptimeClap( if (mem.eql(u8, name, "--" ++ l)) return param; } + // Check aliases + for (param.names.long_aliases) |alias| { + if (mem.eql(u8, name, "--" ++ alias)) + return param; + } } @compileError(name ++ " is not a parameter."); diff --git a/src/deps/zig-clap/clap/streaming.zig b/src/deps/zig-clap/clap/streaming.zig index 40f37a1057..39bfc13262 100644 --- a/src/deps/zig-clap/clap/streaming.zig +++ b/src/deps/zig-clap/clap/streaming.zig @@ -61,9 +61,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { const maybe_value = if (eql_index) |i| arg[i + 1 ..] else null; for (parser.params) |*param| { - const match = param.names.long orelse continue; - - if (!mem.eql(u8, name, match)) + if (!param.names.matchesLong(name)) continue; if (param.takes_value == .none or param.takes_value == .one_optional) {