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
9 changes: 5 additions & 4 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,10 +937,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')
{
// Step over leading flags to reach the subcommand name. A lone `-` is not a
// flag: it is the stdin entry point (`bun - <args>`, as in `node -`), so it
// ends the search like `-e` does. Otherwise the argument after it would be
// taken as the subcommand and `bun - add x` would run `bun add x`.
Comment thread
robobun marked this conversation as resolved.
Outdated
while first_arg_name.len() > 1 && first_arg_name[0] == b'-' && first_arg_name[1] != b'e' {
// `--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
38 changes: 37 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 @@ -276,6 +276,42 @@ describe("echo | bun run -", () => {
group(run);
});

// `-` is the stdin entry point, not a flag: whatever follows it belongs to the
// script (`node - help` runs stdin with argv ["-", "help"]). The subcommand
// lookup used to skip the `-` and dispatch on the next argument instead, so
// `bun - help` printed bun's help and `bun - add x` ran `bun add x`.
describe("bun - <args> runs stdin even when an arg is a subcommand name", () => {
const cases: { flags: string[]; args: string[] }[] = [
{ flags: [], args: ["help"] },
{ flags: [], args: ["add", "--help"] },
{ flags: [], args: ["test"] },
// Landed on `bun run`, which happened to read stdin but dropped "run" from argv.
{ flags: [], args: ["run", "x"] },
// The `-` is reached after stepping over a real flag.
{ flags: ["--silent"], args: ["upgrade", "--help"] },
];

for (const { flags, args } of cases) {
test.concurrent(["bun", ...flags, "-", ...args].join(" "), async () => {
// An empty cwd: if the args are dispatched as a subcommand instead, it
// must not find a project (or test files) to act on.
using dir = tempDir("bun-dash-stdin", {});
await using proc = Bun.spawn({
cmd: [bunExe(), ...flags, "-", ...args],
cwd: String(dir),
env: bunEnv,
stdin: Buffer.from("console.log(JSON.stringify(process.argv.slice(1)))"),
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(["-", ...args]) + "\n");
expect(exitCode).toBe(0);
});
}
});

test("process._eval (undefined for normal run)", async () => {
const cwd = tmpdirSync();
const file = join(cwd, "test.js");
Expand Down