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
92 changes: 50 additions & 42 deletions src/runtime/node/node_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,67 +315,75 @@ mod _impl {
},
);

// Options that take their value from the next argv token (`--conditions x`,
// `-e code`), so that token is not mistaken for the script name. Built lazily
// from the `AUTO_PARAMS` table: `--long` / `-s` for every such param.
// `OneOptional` params (`--inspect`, `-c`) only accept `=value` and leave the
// next token alone, so they are not in the set.
Comment thread
robobun marked this conversation as resolved.
Outdated
static CONSUMES_NEXT_ARG: std::sync::LazyLock<bun_collections::StringSet> =
std::sync::LazyLock::new(|| {
let mut set = bun_collections::StringSet::new();
for param in crate::cli::arguments::AUTO_PARAMS.iter() {
if matches!(
param.takes_value,
bun_clap::Values::One | bun_clap::Values::Many
) {
if let Some(name) = param.names.long {
let mut k = Vec::with_capacity(2 + name.len());
k.extend_from_slice(b"--");
k.extend_from_slice(name);
bun_core::handle_oom(set.insert(&k));
}
if let Some(name) = param.names.short {
bun_core::handle_oom(set.insert(&[b'-', name]));
}
}
}
// Node's whole-token aliases are not params, so they never
// land above; an alias takes a value iff its target does.
Comment thread
robobun marked this conversation as resolved.
Outdated
for (from, to) in crate::cli::arguments::NODE_SHORT_ALIASES {
if set.contains(to) {
bun_core::handle_oom(set.insert(from));
}
}
set
});

let mut seen_run = false;
let mut prev: Option<&[u8]> = None;
let mut awaiting_value = false;

// we re-parse the process argv to extract execArgv, since this is a very uncommon operation
// it isn't worth doing this as a part of the CLI
let mut iter = argv.iter();
let _ = iter.next(); // skip argv[0]
for arg in iter {
// emulate `defer prev = arg` by setting at end of each iteration body
let arg: &[u8] = arg;

// The value of the previous option, however it is spelled (`--conditions -`).
if awaiting_value {
args.push(BunString::clone_utf8(arg));
awaiting_value = false;
continue;
}

// The CLI parses neither of these as a flag: a bare `-` is the script
// positional itself (run stdin) and `--` makes the next token the script,
// so both end execArgv the same way a script name does.
Comment thread
robobun marked this conversation as resolved.
Outdated
if arg == b"-" || arg == b"--" {
break;
}

if arg.len() >= 1 && arg[0] == b'-' {
args.push(BunString::clone_utf8(arg));
prev = Some(arg);
awaiting_value = CONSUMES_NEXT_ARG.contains(arg);
continue;
}

if !seen_run && arg == b"run" {
seen_run = true;
prev = Some(arg);
continue;
}

// A set of execArgv args consume an extra argument, so we do not want to
// confuse these with script names.
// Build the set lazily at runtime from the `AUTO_PARAMS` table:
// `--long` / `-s` for every param with a value.
static MAP: std::sync::LazyLock<bun_collections::StringSet> =
std::sync::LazyLock::new(|| {
let mut set = bun_collections::StringSet::new();
for param in crate::cli::arguments::AUTO_PARAMS.iter() {
if param.takes_value != bun_clap::Values::None {
if let Some(name) = param.names.long {
let mut k = Vec::with_capacity(2 + name.len());
k.extend_from_slice(b"--");
k.extend_from_slice(name);
bun_core::handle_oom(set.insert(&k));
}
if let Some(name) = param.names.short {
bun_core::handle_oom(set.insert(&[b'-', name]));
}
}
}
// Node's whole-token aliases are not params, so they never
// land above; an alias takes a value iff its target does.
for (from, to) in crate::cli::arguments::NODE_SHORT_ALIASES {
if set.contains(to) {
bun_core::handle_oom(set.insert(from));
}
}
set
});

if let Some(p) = prev {
if MAP.contains(p) {
args.push(BunString::clone_utf8(arg));
prev = Some(arg);
continue;
}
}

// we hit the script name
break;
}
Expand Down
39 changes: 38 additions & 1 deletion test/js/node/child_process/child_process.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { semver, write } from "bun";
import { afterAll, beforeEach, describe, expect, it } from "bun:test";
import fs from "fs";
import { bunEnv, bunExe, isLinux, isPosix, isWindows, nodeExe, runBunInstall, shellExe, tmpdirSync } from "harness";
import {
bunEnv,
bunExe,
isLinux,
isPosix,
isWindows,
nodeExe,
runBunInstall,
shellExe,
tempDir,
tmpdirSync,
} from "harness";
import { ChildProcess, exec, execFile, execFileSync, execSync, fork, spawn, spawnSync } from "node:child_process";
import { getEventListeners, once, setMaxListeners } from "node:events";
import { promisify } from "node:util";
Expand Down Expand Up @@ -194,6 +205,32 @@ describe("fork() IPC", () => {
});
expect(exitCode).toBe(0);
});

// fork() launches the child with process.execArgv in front of the module path. For a parent
// started as `bun run -`, execArgv used to contain that "-", so the child ran stdin instead.
it("works from a parent script piped into `bun run -`", async () => {
using dir = tempDir("fork-from-stdin", {
"child.js": `console.log("child " + JSON.stringify(process.argv.slice(2)));`,
});
const parent = `
const child = require("child_process").fork("./child.js", ["x"], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
child.on("exit", code => console.log("child exit " + code));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "-", "parent-arg"],
cwd: String(dir),
env: bunEnv,
stdin: Buffer.from(parent),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.split("\n").filter(Boolean), stderr }).toEqual({
stdout: ['child ["x"]', "child exit 0"],
stderr: "",
});
expect(exitCode).toBe(0);
});
});

describe("spawn()", () => {
Expand Down
32 changes: 27 additions & 5 deletions test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1542,17 +1542,39 @@ it("process.hasUncaughtExceptionCaptureCallback", () => {
});

it("process.execArgv", async () => {
const script = join(__dirname, "print-process-execArgv.js");
// Every command below also gets `script` on stdin, so a bare `-` in the script
// position runs the same fixture from stdin. `argv` is process.argv.slice(2).
const fixtures = [
["index.ts --bun -a -b -c", [], ["--bun", "-a", "-b", "-c"]],
["--bun index.ts index.ts", ["--bun"], ["index.ts"]],
["run -e bruh -b index.ts foo -a -b -c", ["-e", "bruh", "-b"], ["foo", "-a", "-b", "-c"]],
// `-` is the stdin script, not an exec arg, and nothing after it is either.
["run - a b", [], ["a", "b"]],
["--smol run - -x --foo", ["--smol"], ["-x", "--foo"]],
["run --smol - a", ["--smol"], ["a"]],
["--smol - foo", ["--smol"], ["foo"]],
// `--` ends the exec args (node drops it from execArgv too).
["--smol -- index.ts a", ["--smol"], ["a"]],
["run --smol -- - a", ["--smol"], ["a"]],
["--bun node --no-warnings -- index.ts a", ["--no-warnings"], ["a"]],
// ...unless they are the value of an option that takes one (and so is a value spelled `run`).
["--conditions - index.ts", ["--conditions", "-"], []],
["--conditions -- index.ts", ["--conditions", "--"], []],
["--conditions run index.ts", ["--conditions", "run"], []],
// `-c`/`--config` only take a value as `--config=path`, so the next arg is the script.
["-c index.ts a", ["-c"], ["a"]],
];

for (const [cmd, execArgv, argv] of fixtures) {
const replacedCmd = cmd.replace("index.ts", Bun.$.escape(join(__dirname, "print-process-execArgv.js")));
const result = await Bun.$`${bunExe()} ${{ raw: replacedCmd }}`.json();
expect(result, `bun ${cmd}`).toEqual({ execArgv, argv });
}
const results = await Promise.all(
fixtures.map(async ([cmd]) => {
const replacedCmd = cmd.replace("index.ts", Bun.$.escape(script));
return [cmd, await Bun.$`${bunExe()} ${{ raw: replacedCmd }} < ${script}`.json()];
}),
);
expect(Object.fromEntries(results)).toEqual(
Object.fromEntries(fixtures.map(([cmd, execArgv, argv]) => [cmd, { execArgv, argv }])),
);
});

describe("process.exitCode", () => {
Expand Down