Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
22 changes: 15 additions & 7 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,15 @@ pub mod command {
strings::contains_char(basename, b'.')
}

/// `-e` and `-p` take a value, so every `-e…` / `-p…` token (node's `-pe`
/// included) is an eval flag; clusters such as `-bp` are not recognized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

#[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 +946,12 @@ 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 is the script and its arguments: `bun -p test` evaluates
// `test` (https://github.com/oven-sh/bun/issues/23631).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

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 +1079,6 @@ pub mod command {
}
return Tag::AutoCommand;
}
if x == RootCommandMatcher::case(b"-e") {
return Tag::AutoCommand;
}
Tag::AutoCommand
}

Expand Down
67 changes: 66 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,71 @@ for (const flag of ["-e", "--print"]) {
});
}

// https://github.com/oven-sh/bun/issues/23631
// Each child runs in an empty directory so that a token dispatched as a
// subcommand (`bun test`, `bun x`, ...) has 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