Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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` /
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1068,9 +1077,6 @@ pub mod command {
}
return Tag::AutoCommand;
}
if x == RootCommandMatcher::case(b"-e") {
return Tag::AutoCommand;
}
Tag::AutoCommand
}

Expand Down
66 changes: 65 additions & 1 deletion test/cli/run/run-eval.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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();
Expand Down
Loading