Skip to content

fix: preserve -- in process.argv when running script files#30945

Open
remorses wants to merge 1 commit into
oven-sh:mainfrom
remorses:claude/fix-double-dash-argv
Open

fix: preserve -- in process.argv when running script files#30945
remorses wants to merge 1 commit into
oven-sh:mainfrom
remorses:claude/fix-double-dash-argv

Conversation

@remorses
Copy link
Copy Markdown
Contributor

@remorses remorses commented May 17, 2026

What does this PR do?

Fixes #13984

Node.js preserves -- in process.argv when running a script file:

$ node script.js -- rest
# process.argv = ["node", "script.js", "--", "rest"]

$ bun script.js -- rest
# process.argv = ["bun", "script.js", "rest"]  ← '--' was stripped

The fix has two parts:

1. Clap layer (src/clap/comptime.rs + .zig): removed the -- stripping from the stop_after_positional_at codepath so ctx.passthrough preserves all remaining args. This means process.argv (set via vm.argv = ctx.passthrough) now includes --, matching Node.js.

2. Non-process.argv consumers: added strip_leading_double_dash() at the sites that forward passthrough args to package scripts or binaries, preserving the npm/yarn -- separator semantics:

  • run_package_script_foreground call site (package.json scripts)
  • run_binary_without_bunx_path (node_modules/.bin binaries)
  • BunXFastPath::try_launch command-line building (Windows bunx)
  • filter_run.rs (--filter scripts)
  • multi_run.rs (--parallel/--sequential script names)

The -e/--eval case is unaffected; -- is consumed earlier by the streaming parser as a positional separator (matching Node.js).

How did you verify your code works?

Added tests in test/cli/run/run_command.test.ts covering:

  • bun -e "code" -- args (separator consumed, matches Node.js)
  • bun run script.js -- rest (preserved in argv)
  • bun run script.js -- abc -- def (multiple -- all preserved)
  • bun run <pkg-script> -- args (separator consumed, npm compat)

Verified tests fail against system Bun (reproduces the bug) before the fix.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Review Change Stack

Walkthrough

Parser now preserves a leading -- in remaining args for passthrough positionals; a new helper consumes npm-style leading -- when forwarding to child processes; runtime call sites updated; tests added validating -- behavior across CLI modes.

Changes

Process argv -- preservation and npm-style forwarding

Layer / File(s) Summary
Fix argument passthrough to preserve --
src/clap/comptime.rs, src/clap/comptime.zig
When positional parsing stops at stop_after_positional_at, the remaining args are taken verbatim from the iterator so a leading "--" is preserved in passthrough_positionals.
Add strip_leading_double_dash and apply in run paths
src/runtime/cli/run_command.rs
Introduce strip_leading_double_dash and use it when spawning binaries, running package.json scripts, and building the Windows .bunx synthetic command-line so npm-style leading -- is consumed when forwarding args.
Runtime script/filter integration
src/runtime/cli/filter_run.rs, src/runtime/cli/multi_run.rs
Runtime script-building and multi-run logic now iterate over the stripped passthrough slice when embedding forwarded args or collecting extra script names.
Tests: verify -- handling in CLI modes
test/cli/run/run_command.test.ts
Extend harness import and add tests that spawn Bun in multiple modes (bun -e, bun run with script file, multiple --, and package script) asserting exact process.argv slices and exit codes.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main objective: preserving -- in process.argv when running script files, which is the core fix addressed across all modified files.
Description check ✅ Passed The description follows the template structure, provides clear context about the bug fix with code examples, explains the two-part solution strategy, and includes verification details with test coverage.
Linked Issues check ✅ Passed The PR directly addresses issue #13984 by preserving -- in process.argv for script files, matching Node.js behavior. The implementation covers all necessary scenarios: script execution preserves -- while package scripts and eval consume it appropriately.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the -- preservation issue. The clap layer changes ensure -- is preserved, while the strip_leading_double_dash utility ensures npm/yarn semantics are maintained at appropriate forwarding points.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/run/run_command.test.ts`:
- Around line 39-49: Add a stderr guard before the exit-code assertion for each
spawnSync case: when checking the child exit you must insert if (exitCode !== 0)
{ expect(stderr.toString()).toBe(""); } immediately before each
expect(exitCode).toBe(0) so failures print stderr; update the block that calls
spawnSync (referencing spawnSync, exitCode, stderr, stdout) and the other
similar test blocks noted (the cases around the test assertions) to include that
guard right before the final exit-code assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 554e1cf9-df15-450c-a7c3-1d1a15da9202

📥 Commits

Reviewing files that changed from the base of the PR and between 172afa5 and 4093453.

📒 Files selected for processing (2)
  • src/clap/comptime.rs
  • test/cli/run/run_command.test.ts

Comment on lines +39 to +49
const { exitCode, stdout, stderr } = spawnSync({
cmd: [bunExe(), "-e", "console.log(JSON.stringify(process.argv))", "--", "rest", "--foo=bar"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const argv = JSON.parse(stdout.toString());
// With -e, '--' is consumed as a separator (same as Node.js)
expect(argv.slice(1)).toEqual(["rest", "--foo=bar"]);
expect(exitCode).toBe(0);
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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add Bun’s subprocess failure-diagnostics guard before exit-code assertions.

Please add if (exitCode !== 0) { expect(stderr.toString()).toBe(""); } immediately before each expect(exitCode).toBe(0) so failures surface stderr clearly in CI diffs.

Based on learnings: when spawning subprocesses in Bun tests, place the stderr guard immediately before the final expect(exitCode).toBe(0) assertion for better failure output.

Also applies to: 57-69, 77-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/run/run_command.test.ts` around lines 39 - 49, Add a stderr guard
before the exit-code assertion for each spawnSync case: when checking the child
exit you must insert if (exitCode !== 0) { expect(stderr.toString()).toBe(""); }
immediately before each expect(exitCode).toBe(0) so failures print stderr;
update the block that calls spawnSync (referencing spawnSync, exitCode, stderr,
stdout) and the other similar test blocks noted (the cases around the test
assertions) to include that guard right before the final exit-code assertion.

@remorses remorses force-pushed the claude/fix-double-dash-argv branch from 4093453 to 29e71a2 Compare May 17, 2026 20:52
@Jarred-Sumner
Copy link
Copy Markdown
Collaborator

@claude review

Comment thread src/clap/comptime.rs
Comment on lines 589 to 598
if param.names.long.is_none() && param.names.short.is_none() {
pos.push(arg.value.unwrap());
if opt.stop_after_positional_at > 0 && pos.len() >= opt.stop_after_positional_at {
let mut remaining_ = stream.iter.remain();
// PORT NOTE: Zig called `bun.span` (NUL-scan) on `[:0]const u8` argv
// entries. Our `ArgIter` already yields sized `&[u8]`, so `span` is a
// no-op and is dropped.
let first: &[u8] = if !remaining_.is_empty() {
remaining_[0]
} else {
b""
};
if !first.is_empty() && first == b"--" {
remaining_ = &remaining_[1..];
}
// Preserve all remaining args including '--' so it
// appears in process.argv, matching Node.js behavior.
// See: https://github.com/oven-sh/bun/issues/13984
let remaining_ = stream.iter.remain();

passthrough_positionals.reserve_exact(remaining_.len());
for arg_ in remaining_ {
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.

🔴 This change is at the shared clap layer, so it also changes bun run <package.json-script> -- args and bun run <bin> -- args, not just script files. After this PR, bun run dev -- --port 3000 (where dev is e.g. "vite") appends -- --port 3000 to the shell command instead of --port 3000, which diverges from npm and breaks the very common npm run script -- args habit. The leading -- should be stripped in the package-script / binary / filter / multi-run consumers (or the preservation moved to where vm.argv is populated) rather than removed wholesale from clap.

Extended reasoning...

What changed and why it leaks

The fix removes the leading--- strip from the stop_after_positional_at codepath in comptime.rs. That codepath produces passthrough_positionals, which is exposed via args.remaining() and assigned once to ctx.passthrough at src/runtime/cli/Arguments.rs:944. ctx.passthrough is then consumed by every bun run execution mode, not just script-file execution:

  • run_command.rs:963 / :1190vm.argv = take(ctx.passthrough)process.argv for script files (the intended fix target)
  • run_command.rs:2515run_package_script_foreground (loop at :273) — appends each passthrough item to the shell command for package.json scripts
  • run_command.rs:2141-2145run_binary_without_bunx_path builds argv = [executable, ...passthrough] for node_modules/.bin executables
  • filter_run.rs:820-829 — same shell-concat pattern for --filter
  • multi_run.rs:778-781 — pushes each passthrough item as a script name for --parallel/--sequential

None of these downstream consumers strip a leading -- (grep for b"--" in run_command.rs returns no relevant hits), so the new -- flows straight through.

Step-by-step proof: bun run dev -- --port 3000

Given a package.json with "scripts": { "dev": "vite" }:

  1. RunCommand parses with stop_after_positional_at = 2 (Arguments.rs:760). Positionals = ["run", "dev"], then parsing stops.
  2. stream.iter.remain() = ["--", "--port", "3000"].
    • Before this PR: leading "--" was dropped → passthrough = ["--port", "3000"].
    • After this PR: passthrough = ["--", "--port", "3000"].
  3. Arguments.rs:944: ctx.passthrough = ["--", "--port", "3000"].
  4. run_command.rs resolves dev as a package.json script (not a file), clones ctx.passthrough at :2515, and calls run_package_script_foreground.
  5. At :273, each part is appended space-separated to the script body: copy_script = "vite -- --port 3000" (was "vite --port 3000" before).
  6. The shell runs vite -- --port 3000. Vite (and most CLIs) treats -- as end-of-options, so --port and 3000 become positional file arguments instead of the port option — the user's dev server starts on the default port (or errors on the unknown entry).

The same trace applies to run_binary_without_bunx_path: bun run prettier -- file.js now execs ["prettier", "--", "file.js"] instead of ["prettier", "file.js"]. And in multi_run.rs:778, bun run --parallel a b -- c would now push "--" as a script name → Script not found: --.

Why nothing prevents it

The old behavior — stripping the first -- separator before forwarding to scripts/binaries — matched npm run <script> -- <args> semantics, where -- is a CLI-level separator that npm consumes. There is no compensating strip anywhere downstream of ctx.passthrough; the only place -- was ever removed is the line this PR deletes. The PR's new tests only exercise the script-file → process.argv path; there is no test covering bun run <pkg-script> -- args or bun run <bin> -- args, so CI won't catch this.

Impact

This is a user-visible regression in an extremely common workflow (bun run <script> -- <flags> is muscle memory for anyone coming from npm/yarn). It silently changes how flags are delivered to dev servers, formatters, test runners, etc., and diverges from npm. The PR title/description only mention script files, so this appears unintended.

Suggested fix

Keep the clap change (it's correct for process.argv), and strip a single leading "--" from passthrough at the four non-vm.argv consumption points (run_package_script_foreground call site at run_command.rs:2515, run_binary_without_bunx_path at :2141, filter_run.rs:820, multi_run.rs:778). Alternatively, leave clap as-is on main and instead preserve -- only where vm.argv is populated (run_command.rs:963/:1190). Either way, please add a test for bun run <pkg-script> -- arg to lock the behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in latest push. Now stripping -- at the downstream consumers instead of at the clap layer.

Comment thread test/cli/run/run_command.test.ts Outdated
Comment on lines +38 to +40
test("bun script.js -- rest", () => {
const { exitCode, stdout, stderr } = spawnSync({
cmd: [bunExe(), "-e", "console.log(JSON.stringify(process.argv))", "--", "rest", "--foo=bar"],
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.

🟡 nit: this test is named bun script.js -- rest but actually invokes bun -e (and asserts that -- is stripped, not preserved). Consider renaming to something like bun -e <code> -- rest (separator consumed) so the test name matches what it exercises. The third test has a similar minor mismatch — it's named bun script.js -- ... but invokes bun run test.js.

Extended reasoning...

What's wrong

The test on line 38 is named "bun script.js -- rest", which implies it tests the AutoCommand / script-file codepath (bun script.js). But the actual command on line 40 is:

cmd: [bunExe(), "-e", "console.log(JSON.stringify(process.argv))", "--", "rest", "--foo=bar"]

This exercises the -e/eval codepath instead. The test body even acknowledges this on line 47: "With -e, '--' is consumed as a separator (same as Node.js)" — and the assertion on line 48 checks that -- is absent from argv, which is the opposite of what the surrounding describe("should preserve '--' in process.argv") block title suggests.

Why it's confusing

Walking through what a reader sees:

  1. describe block says: "should preserve '--' in process.argv"
  2. First test name says: "bun script.js -- rest"
  3. A reader skimming CI output for a failure on this test will assume it's the script-file passthrough case (the thing this PR actually fixes)
  4. But the test body runs bun -e, hits the streaming-parser separator path (where stop_after_positional_at never fires), and asserts -- is stripped — a totally different codepath with the opposite expected behavior

So if this test fails in CI, the failure message (bun > should preserve '--' in process.argv > bun script.js -- rest) actively misleads about which codepath broke.

The third test (line 72, "bun script.js -- with multiple double dashes") has a milder version of the same issue: it's named bun script.js but actually runs bun run test.js. That distinction matters less since both go through stop_after_positional_at, but it's still inaccurate.

Impact

None on correctness — the assertions are valid for what's actually being run, and the test passes. This is purely a readability/debuggability concern.

Suggested fix

Rename the first test to something like "bun -e <code> -- rest (separator consumed)" and the third to "bun run script.js -- with multiple double dashes". Alternatively, change the first test's body to actually run a script file via bun script.js (no run subcommand), which would also add coverage for the AutoCommand path that currently isn't tested here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: renamed tests to match what they actually exercise.

@remorses remorses force-pushed the claude/fix-double-dash-argv branch from 29e71a2 to 67f5149 Compare May 18, 2026 07:35
Node.js preserves `--` in process.argv when running a script file
(e.g. `node script.js -- rest` gives `["node", "script.js", "--", "rest"]`).
Bun was stripping it in the CLI arg parser.

The bug was in both `src/clap/comptime.rs` and `src/clap/comptime.zig` in the
`stop_after_positional_at` codepath. After identifying the first positional arg
(the script filename), the code collected remaining args as passthrough but
explicitly checked if the first remaining arg was `--` and skipped it. This
caused `--` to disappear from process.argv.

The fix has two parts:

1. Remove the `--` stripping from the clap layer (`comptime.rs`/`.zig`) so
   `ctx.passthrough` preserves all remaining args including `--`. This means
   `process.argv` (set via `vm.argv = ctx.passthrough`) now matches Node.js.

2. Strip leading `--` at the non-process.argv consumers that forward args to
   package scripts or binaries, matching npm/yarn `--` separator semantics:
   - `run_package_script_foreground` call site (package.json scripts)
   - `run_package_script_foreground` Bun shell path (swap ctx.passthrough
     for shell $1/$2 positional expansion)
   - `run_binary_without_bunx_path` (node_modules/.bin binaries)
   - `BunXFastPath::try_launch` command-line building (Windows bunx)
   - `filter_run.rs` (--filter scripts)
   - `multi_run.rs` (--parallel/--sequential script names)

The `-e`/`--eval` case is unaffected because `--` is consumed earlier by the
streaming parser as a positional separator (matching Node.js behavior where
`node -e "code" -- args` also strips `--`).

Fixes oven-sh#13984

Session: ses_1c8657c16ffev9oftdw66OjYxq
@remorses remorses force-pushed the claude/fix-double-dash-argv branch from 67f5149 to 08fe43e Compare May 18, 2026 14:08
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
test/cli/run/run_command.test.ts (1)

38-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add stderr guard before each exitCode assertion.

All five test cases are missing the stderr diagnostic guard. Add if (exitCode !== 0) { expect(stderr.toString()).toBe(""); } immediately before each expect(exitCode).toBe(0) assertion (lines 49, 69, 87, 114, 137) to surface stderr clearly in CI diffs when tests fail.

Based on learnings: when spawning subprocesses in Bun tests, place the stderr guard immediately before the final expect(exitCode).toBe(0) assertion for better failure output.

Also applies to: 52-70, 72-88, 90-115, 117-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/run/run_command.test.ts` around lines 38 - 50, Add a stderr
diagnostic guard immediately before each final exit assertion in the tests that
call spawnSync: for each test that does const { exitCode, stdout, stderr } =
spawnSync(...) (notably the "bun -e <code> -- rest (separator consumed)" test
and the four other similar tests), insert if (exitCode !== 0) {
expect(stderr.toString()).toBe(""); } directly before expect(exitCode).toBe(0)
so failed subprocesses surface stderr in CI diffs; update all occurrences that
reference exitCode/stderr from spawnSync accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@test/cli/run/run_command.test.ts`:
- Around line 38-50: Add a stderr diagnostic guard immediately before each final
exit assertion in the tests that call spawnSync: for each test that does const {
exitCode, stdout, stderr } = spawnSync(...) (notably the "bun -e <code> -- rest
(separator consumed)" test and the four other similar tests), insert if
(exitCode !== 0) { expect(stderr.toString()).toBe(""); } directly before
expect(exitCode).toBe(0) so failed subprocesses surface stderr in CI diffs;
update all occurrences that reference exitCode/stderr from spawnSync
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 63168d13-9626-4884-8194-90d7312a4c2b

📥 Commits

Reviewing files that changed from the base of the PR and between 67f5149 and 08fe43e.

📒 Files selected for processing (6)
  • src/clap/comptime.rs
  • src/clap/comptime.zig
  • src/runtime/cli/filter_run.rs
  • src/runtime/cli/multi_run.rs
  • src/runtime/cli/run_command.rs
  • test/cli/run/run_command.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun strips '--' from process.argv

2 participants