Skip to content

cli: evaluate bun -p test / bun --print test / bun -pe help instead of dispatching the code as a subcommand - #38578

Open
robobun wants to merge 3 commits into
mainfrom
farm/d0ee0944/which-eval-flags
Open

cli: evaluate bun -p test / bun --print test / bun -pe help instead of dispatching the code as a subcommand#38578
robobun wants to merge 3 commits into
mainfrom
farm/d0ee0944/which-eval-flags

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Addresses the bun --print x half of #23631. Its other half (bun run --eval / bun run --print print the bun run usage) is a different code path and is not changed here.

Problem

  • bun -p test, bun --print test and bun --eval test start the test runner (bun test v1.4.0 ... No tests found!) instead of evaluating test; bun --print x prints the bunx usage (bun --print may interpret what follows as a sub-command w/ no way to escape && bun run --eval is broken #23631); bun -pe help prints bun's help. bun -e test is the only spelling that evaluates (ReferenceError, as node does for all of them).
  • The attached-value forms are misrouted through the script's first argument: bun -p=1 test, bun -p1 test, bun --print=1 test and bun --eval=1 test all run the test runner. Any subcommand name in that position is dispatched: bun -p a foo is bun add foo (it writes package.json and hits the registry), bun -p i is bun install.
  • Cause: Command::which() (src/runtime/cli/mod.rs, the while loop before the RootCommandMatcher lookup) steps over every leading - token to find the subcommand name, and its only stop condition is a token starting with -e. -p, -pe, --print and --eval are stepped over like boolean flags, so the token after them (the code, or the first script argument when the code is attached) is what gets matched against the subcommand names.

Fix

  • which() now stops at any eval flag spelling and returns AutoCommand: -e..., -p... (covers -p <code>, -p<code>, -p=<code> and node's -pe), --eval, --eval=..., --print, --print=... (new is_eval_flag).
  • Correct because this is the set of tokens arguments::parse reads as an eval for AutoCommand, and once one of them is present, everything after it is the script or its argv by definition, so there is no subcommand to look for. -e and -p take a value, so a token starting with either is always that flag with an attached value (clap reads -pe as -p via NODE_SHORT_ALIASES, -p1+1 as -p with 1+1); the prefix rule is exactly how the token will be parsed. Matches node, where node -p test / node -pe help / node --print=1 test never do anything but evaluate.
  • The -e entry in the keyword matcher is removed: it was only reachable through the old carve-out (the loop now returns before the matcher for any flag token).
  • Behaviour change to be aware of: bun -p install and friends used to reach the install command, which then read -p as --production. That was an accident of the skip loop; bun -p <x> is documented as print-eval and bun install -p / --production are unchanged. Short clusters such as -bp <code> are not recognised by which(); they were not before either (bun -bp test errors in the test runner today) and node rejects them outright.
  • Verified with test/cli/run/run-eval.test.ts (eval flags take precedence over subcommand names, including the issue's literal bun --print x): 9 of the 12 new cases fail on the unfixed binary (the -e, -e= and -p <non-keyword> test cases are controls that already passed) and all pass with the fix; the rest of the file, test/cli/run/as-node.test.ts and test/cli/run/crash-report-command-char.test.ts still pass.

Background

  • Command::which() classifies argv into a command Tag before any argument parsing happens. Because runtime flags may precede the subcommand (bun --bun test, and BUN_OPTIONS is spliced in right after argv0), it skips leading - tokens and matches the first remaining token against the subcommand names (test, help, x, add, i, ...). Anything that does not match is AutoCommand.
  • AutoCommand is the bun <file> / bun -e <code> path: arguments::parse parses argv with AUTO_PARAMS (where -e/--eval and -p/--print take a value and -pe is aliased to -p), and exec_auto_or_run runs the eval when one was given. That side already handled every spelling; only the classifier in front of it did not.
  • Related but separate open fixes to the same loop: cli: do not skip a lone - when picking the subcommand (bun - add x ran bun add x) #38568 stops it at a lone - (stdin), cli: classify the subcommand past --cwd/--env-file values #36644 steps over the values of --cwd / --env-file. Neither covers the eval flags, which must stop the search rather than be stepped over (bun -p 1 test has to evaluate 1 with test as a script argument).

Command::which() steps over the flags in front of the subcommand name.
It only stopped at tokens starting with -e, so for -p, -pe, --print and
--eval the next argv token was matched against the subcommand names:
`bun -p test` ran the test runner, `bun -pe help` printed the help, and
with an attached value (`bun -p=1 test`, `bun --print=1 test`) the
script's first argument was dispatched the same way.

Any eval flag spelling now classifies the invocation as AutoCommand,
where arguments::parse already handles these flags; the -e entry in the
keyword matcher was only reachable through the old carve-out.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c15744b1-0502-4b0f-9a25-14c3ceabe43e

📥 Commits

Reviewing files that changed from the base of the PR and between d8a9fc3 and eecba16.

📒 Files selected for processing (2)
  • src/runtime/cli/mod.rs
  • test/cli/run/run-eval.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2dd4357e-b469-47ac-b38c-a500c56e9002

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and d8a9fc3.

📒 Files selected for processing (2)
  • src/runtime/cli/mod.rs
  • test/cli/run/run-eval.test.ts

Walkthrough

The CLI now recognizes eval and print flag variants during command detection. New tests verify that these flags take precedence over subcommand-like code and arguments.

Changes

Eval flag precedence

Layer / File(s) Summary
Eval and print flag classification
src/runtime/cli/mod.rs
is_eval_flag recognizes short and long eval/print forms. which() routes these flags to AutoCommand and removes the redundant -e fallback.
Eval and print precedence tests
test/cli/run/run-eval.test.ts
Tests cover combined flags, equals forms, skipped flags, subcommand-like code, and trailing arguments. Assertions verify output, errors, and exit codes.

Suggested reviewers: cirospaciari, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and specifically summarizes the primary CLI behavior change for eval and print flags.
Description check ✅ Passed The description fully explains the problem, fix, scope, behavior changes, and verification, despite not using the template headings.

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed, waiting on CI.

Reproduced on bun 1.4.0-canary (b7a0431) and a debug build of main (eabb96d) in an empty directory: bun -p test, bun --print test, bun --eval test, bun -p=1 test, bun -p1 test, bun --print=1 test and bun --eval=1 test all start the test runner, bun --print x prints the bunx usage (the repro in #23631), bun -pe help prints bun's help, bun -p a --help prints bun add's help. bun -e test evaluates (ReferenceError), as node does for every one of these.

Test: test/cli/run/run-eval.test.ts, eval flags take precedence over subcommand names. 9 of the 12 cases fail on the unfixed binary, all 12 pass with this branch (bun bd test test/cli/run/run-eval.test.ts, 49 pass).

Scope: this fixes the bun --print x half of #23631. The bun run --eval / bun run --print half is a separate code path (exec_auto_or_run only runs the eval for AutoCommand) and is being handled separately.

@claude claude Bot left a comment

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.

I reviewed this PR and didn't find any bugs. The is_eval_flag predicate and the loop restructuring look correct, and the test matrix is thorough. Because this changes argv→subcommand classification in Command::which() (every invocation goes through it), intentionally flips the meaning of bun -p <subcommand>, and overlaps with two other open PRs touching the same loop (#38568, #36644), a human sign-off on the behavior change and merge ordering would still be worthwhile.

What was reviewed:

  • is_eval_flag pattern coverage vs. AUTO_PARAMS / NODE_SHORT_ALIASES: [b'-', b'e' | b'p', ..] matches -e/-p/attached-value/-pe and nothing else (second byte excludes --, lone -); long forms are exact-match plus =-prefix only.
  • Confirmed the removed RootCommandMatcher::case(b"-e") arm is unreachable now that the loop returns before the matcher for any - token that is an eval flag.
  • Checked the empty-eval and looks_like_run_entrypoint fast paths in start() — both run before which() and are unaffected.
  • Tests: 12 concurrent spawns in an empty tempDir, pipes drained via Promise.all, assertions on exact stderr/stdout/exitCode.
Extended reasoning...

Overview

The PR fixes Command::which() in src/runtime/cli/mod.rs so that every spelling of the eval/print flags (-e, -p, -pe, --eval, --print, and their attached-value forms) stops the leading-flag skip loop and returns Tag::AutoCommand, instead of only the -e prefix doing so. Previously -p, --print and --eval were stepped over as if they were boolean flags, so the code (or the first script argument) was matched against the subcommand table — bun -p test ran the test runner, bun -p a foo ran bun add foo. A new is_eval_flag helper encapsulates the predicate, the now-dead RootCommandMatcher::case(b"-e") arm is removed, and 12 new test cases in test/cli/run/run-eval.test.ts cover both the "code is a subcommand name" and "script argument is a subcommand name" shapes.

Security risks

None introduced. If anything this closes a footgun: bun -p a foo previously reached bun add foo and wrote to package.json / hit the registry, which is the more dangerous direction. The new behavior only ever routes to AutoCommand, which is what arguments::parse was already prepared to handle for these tokens.

Level of scrutiny

High. which() is called on every bun invocation and its output selects the entire subcommand path. The change is small and mechanically sound (I traced the matches! slice pattern against the actual AUTO_PARAMS definitions of -e/--eval and -p/--print and the NODE_SHORT_ALIASES entry for -pe), but it deliberately changes user-visible behavior: bun -p install now evaluates install instead of running bun install --production. The PR description argues — convincingly — that the old behavior was an accident of the skip loop and that bun install -p is unaffected, but a maintainer should confirm that trade-off is acceptable.

Other factors

  • The PR notes two other open PRs (#38568 for lone -, #36644 for --cwd/--env-file value stepping) that edit the same loop; merge order matters and a human should coordinate.
  • I verified the removed case(b"-e") branch is truly dead: any token reaching the matcher no longer starts with - (the loop either returns or advances past every - token), so a -e literal can never reach RootCommandMatcher::match.
  • The new tests follow harness conventions (tempDir, await using, Promise.all on stdout/stderr/exited, test.concurrent, exact-value assertions) and each child runs in an empty cwd so a misdispatched install/add would have nothing to act on.
  • No prior review comments to address; only a CodeRabbit rate-limit notice.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

On the two points the review asks a maintainer to confirm:

  • Behaviour change: the only invocations that change are a -p/-e-prefixed token, --eval or --print placed in front of a subcommand name (bun -p install, bun -p i, bun -p outdated, ...). Those reached the install-family command by accident and had -p read as --production there. bun install -p, bun install --production and every other flag-before-subcommand form (bun --bun test, BUN_OPTIONS=... bun test) are unchanged. bun -p <x> has only ever been documented as print-eval, which is also what node does with it.
  • Merge order with cli: do not skip a lone - when picking the subcommand (bun - add x ran bun add x) #38568 and cli: classify the subcommand past --cwd/--env-file values #36644: I merged each of them into this branch locally. Both conflict only in the single hunk at the head of the while loop in which(); the test files merge cleanly. The three changes are independent conditions on the same loop (stop at a lone -, stop at an eval flag, step over the value of --cwd/--env-file), so the resolution is to keep all three in the loop body in any order, and the order they land in does not matter. I will rebase this one if either lands first.

The doc comment claimed every spelling clap accepts is recognized, but
short clusters such as `-bp <code>` are not. Link the issue from the
dispatch check and the tests, and use its literal `bun --print x` as one
of the cases.
Comment thread src/runtime/cli/mod.rs Outdated
Comment on lines +893 to +894
/// `-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

Comment thread src/runtime/cli/mod.rs Outdated
Comment on lines +950 to +951
// 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

@claude claude Bot left a comment

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.

I reviewed this PR and didn't find any bugs. Because it deliberately changes what bun -p <subcommand> does (previously bun -p install reached the install command with --production; now it print-evals install), and the author has already flagged that for maintainer confirmation, a human sign-off on the behaviour change is worth having.

What was reviewed:

  • is_eval_flag prefix match [b'-', b'e' | b'p', ..] — correct because -e/-p take a value in AUTO_PARAMS, so any -e…/-p… token is that flag with an attached value.
  • Removed RootCommandMatcher::case(b"-e") — confirmed unreachable now the loop returns before the matcher for any --prefixed token.
  • Test cases cover detached, attached, =-attached, -pe, and flag-before-eval spellings; the empty-eval fast path in start() is untouched.
Extended reasoning...

Overview

The PR fixes issue #23631: bun -p test, bun --print test, bun --eval test, bun -pe help and the attached-value forms (bun -p=1 test, bun -p1 test) were being dispatched as subcommands instead of evaluated. The fix is a new is_eval_flag() predicate in src/runtime/cli/mod.rs that stops Command::which()'s leading-flag skip loop and returns Tag::AutoCommand when any eval-flag spelling is seen. A now-unreachable -e case in the keyword matcher is removed. Twelve test cases are added to test/cli/run/run-eval.test.ts.

Security risks

None. This is argv classification only; no untrusted-input parsing, no filesystem/network side effects introduced.

Level of scrutiny

Medium. The code change is small and the logic is sound (the prefix match on -e/-p is exactly how arguments::parse reads these tokens, so there is no ambiguity). However, it is a user-visible CLI behaviour change: bun -p install, bun -p i, bun -p outdated and similar previously reached the package-manager commands (with -p read as --production) and now print-eval the identifier instead. The author argues convincingly that this was accidental and undocumented, and it matches Node's behaviour, but the repo's review guidance asks for maintainer agreement on default-behaviour changes.

Other factors

  • The author explicitly flagged the behaviour change for maintainer confirmation in a follow-up comment.
  • Two other open PRs (#38568, #36644) touch the same loop; the author has described the trivial merge resolution but a human may want to pick a landing order.
  • The comment-cop bot left two inline notes on the new comments; both are two-line comments (one a doc comment stating the cluster limitation, one an issue link), which look like false positives to me, but the author may want to address or dismiss them.
  • Tests follow the harness conventions (tempDir, test.concurrent, drain pipes concurrently, assert stderr/stdout before exit code) and 9 of 12 fail on the unfixed binary per the author's verification.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up: #38568 changes the same while header in Command::which() (it stops the loop at a lone -, so bun - help reads stdin instead of printing help), and a test merge of the two branches conflicts on that line. The header here, !first_arg_name.is_empty() && first_arg_name[0] == b'-', still steps over a lone -, so when resolving (in whichever PR lands second) please keep the len() > 1 guard:

while first_arg_name.len() > 1 && first_arg_name[0] == b'-' {
    if is_eval_flag(first_arg_name) {
        return Tag::AutoCommand;
    }
    ...
}

The test blocks are in different parts of run-eval.test.ts and merge cleanly.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant