diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index 9bd01c88ea88..47db997bf0b7 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -890,6 +890,14 @@ pub mod command { strings::contains_char(basename, b'.') } + /// `-e`/`-p` take a value, so any `-e…`/`-p…` token is one; clusters like `-bp` are not. + #[inline] + fn is_eval_flag(arg: &[u8]) -> bool { + matches!(arg, [b'-', b'e' | b'p', ..] | b"--eval" | b"--print") + || arg.starts_with(b"--eval=") + || arg.starts_with(b"--print=") + } + /// `#[inline(never)]`: argv→`Tag` classification, called once from /// `Cli::start` on every `bun` invocation. Kept a concrete symbol so /// `src/startup.order` can place it next to `Cli::start` / @@ -937,10 +945,11 @@ pub mod command { let Some(mut first_arg_name) = iter.next() else { return Tag::AutoCommand; }; - while !first_arg_name.is_empty() - && first_arg_name[0] == b'-' - && !(first_arg_name.len() > 1 && first_arg_name[1] == b'e') - { + while !first_arg_name.is_empty() && first_arg_name[0] == b'-' { + // The rest of argv belongs to the script: https://github.com/oven-sh/bun/issues/23631 + if is_eval_flag(first_arg_name) { + return Tag::AutoCommand; + } // `--interactive` stays on AutoCommand: Arguments.rs parses it and the no-target check // routes to RunCommand::exec_node_repl. An early ReplCommand return here would bypass // that and boot the legacy `bun repl` implementation instead. @@ -1068,9 +1077,6 @@ pub mod command { } return Tag::AutoCommand; } - if x == RootCommandMatcher::case(b"-e") { - return Tag::AutoCommand; - } Tag::AutoCommand } diff --git a/test/cli/run/run-eval.test.ts b/test/cli/run/run-eval.test.ts index 0f928a7ce6a7..b9fca9710a34 100644 --- a/test/cli/run/run-eval.test.ts +++ b/test/cli/run/run-eval.test.ts @@ -1,7 +1,7 @@ import { SyncSubprocess } from "bun"; import { describe, expect, test } from "bun:test"; import { rmSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir, tmpdirSync } from "harness"; import { tmpdir } from "os"; import { join, sep } from "path"; @@ -108,6 +108,70 @@ for (const flag of ["-e", "--print"]) { }); } +// https://github.com/oven-sh/bun/issues/23631 +// Empty cwd: a token dispatched as a subcommand (`bun test`, `bun x`, ...) must find nothing to act on. +describe("eval flags take precedence over subcommand names", () => { + describe("the code is a subcommand name", () => { + const cases: string[][] = [ + ["-p", "test"], + ["--print", "x"], + ["--eval", "test"], + ["-pe", "help"], + ["-e", "test"], + // The eval flag is reached after skipping an unrelated flag. + ["--smol", "-p", "test"], + ]; + + for (const args of cases) { + const name = args[args.length - 1]; + test.concurrent(`bun ${args.join(" ")} evaluates \`${name}\``, async () => { + using dir = tempDir("eval-subcommand-name", {}); + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain(`ReferenceError: ${name} is not defined`); + expect(stdout).toBe(""); + expect(exitCode).toBe(1); + }); + } + }); + + describe("a script argument is a subcommand name", () => { + const argv = "JSON.stringify(process.argv.slice(1))"; + const cases: string[][] = [ + [`-p${argv}`, "test"], + [`-p=${argv}`, "test"], + [`--print=${argv}`, "test"], + [`--eval=console.log(${argv})`, "help"], + [`-e=console.log(${argv})`, "test"], + ["-p", argv, "test"], + ]; + + for (const args of cases) { + const name = args[args.length - 1]; + test.concurrent(`bun ${args.join(" ")} passes \`${name}\` to the script`, async () => { + using dir = tempDir("eval-subcommand-arg", {}); + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe(JSON.stringify([name]) + "\n"); + expect(exitCode).toBe(0); + }); + } + }); +}); + describe("--print for cjs/esm", () => { test("eval result between esm imports", async () => { let cwd = tmpdirSync();