Skip to content

shell: stop pathname expansion of the value in NAME=value cmd and export NAME=value - #47759

Open
robobun wants to merge 2 commits into
mainfrom
robobun/758cbcc9/shell-assign-no-glob
Open

robobun wants to merge 2 commits into
mainfrom
robobun/758cbcc9/shell-assign-no-glob

Conversation

@robobun

@robobun robobun commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun Shell globs an assignment value that goes to a command. DEBUG=* node app.js gives the child file names. export DEBUG=xss:* && mocha fails: bun: no matches found: DEBUG=xss:*.
  • Cause: the two glob starts in Expansion (src/runtime/shell/states/Expansion.rs:249, :344) ignore what the word is for.
  • Shell glob fixes #9940 asked for a test that DEBUG=* bun foo.js gives *. It was not added.

Fix

  • Expansion::init takes a WordContext, not the assign_ctx bool. Both glob starts skip an EnvValue word (NAME=value cmd, export NAME=value): no glob task, no folder read.
  • POSIX 2.9.1: an assignment value gets no pathname expansion. sh and cross-env pass the text.
  • Verified: test/js/bun/shell/bunshell.test.ts (7 new tests, 6 fail on 1.4.2 and main, no existing line changed) and the rest of test/js/bun/shell/. Self-reviewed: 12 concerns raised, 11 addressed (Notes).

Background

Downsides

Notes

Two decisions for a maintainer

  1. Which release takes the first table below. Those commands exit 0 on 1.4.x and give the command another value now, with no error. main is 1.4.4, so a merge ships in the next 1.4.x patch. The alternative is 1.5. The second table has no such row: those commands fail on 1.4.x.
  2. Keep or stop the glob of a bare NAME=value. This PR keeps it, because a landed test pins touch hihello; touch hifriends; FOO=hi*; echo $FOO to the two names (bunshell.test.ts, "Trailing asterisk with matches, inline"). The facts:
    • The matches become one word, joined by spaces (Assigns::child_done). So only echo $FOO, or a pattern with one match, gives what sh gives. FOO=hi*; ls $FOO fails on main and here: ls: hihello hix: No such file or directory.
    • Bun Shell does not glob a value where it is used (shell: only template-literal glob tokens act as pattern syntax #31220). So the bare form and the two forms of this PR stay different unless the bare form stops too.
    • If it stops, FOO=hi*; echo "$FOO" prints hi* as in sh, and FOO=hi*; echo $FOO changes from the names to hi*.
    • The sample below has one bare word with an unquoted *: files=${TEST_FILES:-dist/test/*.test.js}; printf '%s\n' $files | .... It does not run on main or here, because ${NAME:-word} is text.

Outputs

Fixture: hihello, hix, a1, b1, dist/pkg.tgz and a file named FOO=hix. "main" is a release build of 3f3a9ff, "this PR" a release build of the branch. bash is 5.2. dash gives what bash gives, except where a row says so.

Commands that exit 0 on 1.4.x and give another value now:

Script main This PR bash
DEBUG=* printenv DEBUG the six names of the folder * *
F=dist/*.tgz printenv F dist/pkg.tgz dist/*.tgz dist/*.tgz
FOO=*$(echo x) printenv FOO FOO=hix hix *x *x
FOO={a,b}* printenv FOO a* b* a1 b1 a* b* {a,b}*
export FOO=hi* && printenv FOO hix (the word FOO=hi* matches the file FOO=hix) hi* hi*
DEBUG=* true; printenv DEBUG the six names * nothing, exit 1

Commands that fail on 1.4.x and work now:

Script main This PR bash
export DEBUG=xss:* && printenv DEBUG bun: no matches found: DEBUG=xss:*, exit 1 xss:* xss:*
export FOO={a,b}* && printenv FOO bun: no matches found: FOO={a,b}*, exit 1 b* b* (dash: {a,b}*)

Not changed:

Script main and this PR bash
FOO=hi*; echo $FOO hihello hix hihello hix
FOO=hi*; echo "$FOO" hihello hix hi*
FOO=hi*; ls $FOO ls: hihello hix: No such file or directory the two files
FOO={a,b} printenv FOO a b {a,b}
echo FOO=hi* FOO=hix FOO=hix
DEBUG=** printenv DEBUG bun: command not found: DEBUG **
FOO=~/x printenv FOO ~/x $HOME/x

Not changed, on purpose

The list is for the fixture above.

Who is affected

  • Sample: 63,428 unique package.json scripts, from the latest version of 41,513 npm packages (17,147 popular, 24,455 random, 89 in both). 30 assignment words before a command or after export have an unquoted *: 28 DEBUG, 1 NODE_DEBUG, 1 MARKDOWN_INCLUDE. Each is written for sh, which passes the text. Examples: DEBUG=* mocha --reporter list (tiny-lr), DEBUG=* ./bin/homebridge.js -D -P example-plugins/ (homebridge), DEBUG=* bun bin/cli.js (@unhook/cli), export DEBUG=xss:* && mocha -t 5000 (xss).
  • No script in the sample reads file names from such a value.
  • npm run and node --run (both through sh) and cross-env 10.1.0 pass the text. @yarnpkg/shell 4.1.3 and dax-sh 0.44.1 glob these values, as Bun does on main.
  • A script owner can write DEBUG='*' cmd today. A person who runs the script of a published package cannot.

Measurements

Release builds of main (3f3a9ff) and of this branch, Linux x64. The counters are a perf_event reader for instructions:u and branches:u, a ptrace syscall counter, and gdb hit counts on the mimalloc entry points. valgrind, perf and strace are not on the machine.

  • Glob walks per word (debug build, BUN_DEBUG_Glob): DEBUG=* true, export FOO=hi*, FOO={a,b}* true, export FOO={a,b}* and FOO=*$(echo x) true go from 1 to 0. A bare FOO=hi*, echo one*, echo hi > o*.txt and [[ -f o*.txt ]] stay at 1.
  • A fresh bun exec 'DEBUG=* true': 200 to 213 syscalls and 3 threads on main, 132 and 0 here. The quoted DEBUG='*' true is 132 and 0 on both.
  • Allocator calls for one A=* true statement: 44 on main, 16 here. The quoted form is 15 on both. Pool tasks: 1 on main, 0 here.
  • 2000 lines of A=* true in one process: 148.1 ms on main, 5.2 ms here (the minimum of 7 interleaved rounds). The quoted form is 4.2 ms and 4.0 ms, and its middle half spans 1 ms. true a b c is 4.3 ms and 4.6 ms.
  • Words that are not affected: 180,000 plain words (20,000 lines of true a b c d e f g h) cost 348,360,074 instructions on main and 348,000,039 here, and 67,220,542 branches on main and 67,400,569 here. That is 2.0 instructions less and 1.0 branch more for each word. 20,000 lines of A=1 true cost 135,890,327 and 135,850,328 instructions. These are the minimum of 3 interleaved runs. The runs of one build differ by 2,157 instructions at most.
  • The added branch is one jmp: LLVM now places the exit of a one-atom word with no * behind it.
  • A word with a * pays 2 instructions (cmpb, jne) before each of the two glob starts.
  • Interpreter::next_node (it holds Expansion::next): 15,146 bytes and 3,302 instructions on main, 15,133 and 3,309 here. Expansion::init: 618 bytes and 109 instructions on main, 564 and 103 here. ShellGlobTask::run_from_main_thread and Interpreter::child_done do not change. .text is 140,286,972 bytes on both.
  • size_of: WordContext 1 (the bool was 1), Expansion 168, Node 176. All as on main.

Tests

  • The new tests on 1.4.2 and on a release build of main: 6 fail, and the control passes.
  • Each clause of the fix fails a test when it is removed (debug build). With no test at the second glob start: the export test with a brace group. With the bare context for a value before a command: the three tests of that form. With Fields for an export operand: the three export tests.
  • test/js/bun/shell/bunshell.test.ts on the debug build: 471 pass, 3 skip (they need a user that is not root), 0 fail.
  • All pass on the debug build: brace, parse, lex, exec, assignments-in-pipeline, bunshell-default, bunshell-file, bunshell-instance, shell-seq-condexpr, env.positionals, yield, throw, lazy, shelloutput, file-io, pipeline_stack, shell-cmdsub-crash, the 16 files of commands/ and test/cli/run/run-shell.test.ts.
  • commands/ls.test.ts: in one run, recursive > node_modules timed out after 5000 ms. That test waits for a real bun install. A release build of main has the same timeout.

Design

Self-review

Open PRs on the same lines

Conflict regions are from git merge-file --diff3 on the four state files.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/shell/bunshell.test.ts

…export NAME=value`

The value of an assignment that goes into the environment of a command is
not a pattern (POSIX 2.9.1). `DEBUG=* cmd` now gives the command `DEBUG=*`,
and `export DEBUG=xss:* && cmd` no longer fails with `no matches found`.

`Expansion` takes a `WordContext` in place of the `assign_ctx` bool. The two
glob starts read it, so no glob task and no folder read exists for these
words.

The bare form `NAME=value` keeps its glob, and brace expansion is not
changed.
@robobun

robobun commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main and on 1.4.2, in a folder with the two files one.txt and two.txt:

bun exec 'DEBUG=* printenv DEBUG'
bun exec 'export DEBUG=xss:* && printenv DEBUG'
  • main and 1.4.2 print one.txt two.txt for the first command. For the second they print bun: no matches found: DEBUG=xss:*, exit code 1.
  • This PR prints * and xss:*. bash and dash print the same.

The Notes of the description list two decisions for a maintainer: the release that takes the silent change, and the glob of a bare NAME=value.

CI: build 136940 is complete. test/js/bun/shell/bunshell.test.ts passes on all 11 lanes that run it: Debian, Ubuntu and Alpine (x64 and aarch64), the ASAN lane, Windows (x64 and aarch64) and macOS (x64 and aarch64). The tests that fail on each retry are 27 files under test/aot/ and test/bundler/compile-aot-size.test.ts. All 28 also fail on main, in build 136848 at the merge base. The diff does not change that code. The PR is ready for a maintainer to review.

@github-actions github-actions Bot added the claude label Nov 2, 2026
@robobun

robobun commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:51 AM PT - Nov 2nd, 2026

❌ @robobun, your commit f25e477 has 41 failures in Build #136940 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47759

That installs a local version of the PR into your bun-47759 executable, so you can run:

bun-47759 --bun

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 271b65e and 02a2ac3.

Files selected for processing: 6
  • docs/runtime/shell.mdx
  • src/runtime/shell/states/Assigns.rs
  • src/runtime/shell/states/Cmd.rs
  • src/runtime/shell/states/CondExpr.rs
  • src/runtime/shell/states/Expansion.rs
  • test/js/bun/shell/bunshell.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

Bun Shell now distinguishes fields, shell-variable values, and command environment values during expansion. Command environment assignments do not undergo pathname expansion. Call sites select the applicable context, and tests and documentation cover assignment values.

Changes

Assignment expansion behavior

Layer / File(s) Summary
Define context-specific expansion rules
src/runtime/shell/states/Expansion.rs
WordContext distinguishes fields, shell-variable values, and command environment values. Pathname expansion applies to fields and shell-variable values, but not command environment values. Command-substitution field splitting applies only to fields.
Select contexts at expansion call sites
src/runtime/shell/states/Assigns.rs, src/runtime/shell/states/Cmd.rs, src/runtime/shell/states/CondExpr.rs
The shell states pass contexts for shell and command assignments, export operands, redirects, ordinary arguments, and conditional-expression arguments.
Test and document assignment behavior
test/js/bun/shell/bunshell.test.ts, docs/runtime/shell.mdx
Tests cover command assignment values, export operands, ordinary arguments, and package scripts. The documentation states that glob characters remain literal in assignment values.

Possibly related PRs

  • oven-sh/bun#33423: Changes brace and pathname expansion in Expansion, including the expansion path that this PR updates.
  • oven-sh/bun#47338: Refactors the shell expansion API and assignment-context argument at the same call sites.
  • oven-sh/bun#46691: Adds parameter-word expansion and defines assignment-word expansion behavior in Expansion.

Suggested reviewers: jarred-sumner, alii

Merge Risk: Minimal · up to 02a2a

The change suppresses pathname expansion in command environment assignments and literal export operands. No actionable merge-blocking issue was established; the proposed dynamic-export test concerns unchanged behavior outside the defined regression coverage.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the main change: assignment values in NAME=value cmd and export NAME=value no longer undergo pathname expansion. The raw summary and objectives confirm this change in Expansion, As…
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.
Description check Passed The description is thorough and follows the template. It has the required "What does this PR do?" content, covered by the Problem, Fix, Background, and Downsides sections. It also has the required "Ho…
Full details: Title check

Explanation

The title names the main change: assignment values in NAME=value cmd and export NAME=value no longer undergo pathname expansion. The raw summary and objectives confirm this change in Expansion, Assigns, Cmd, and the shell tests. The title is specific, concise, and single-sentence, so it passes.

Full details: Description check

Explanation

The description is thorough and follows the template. It has the required "What does this PR do?" content, covered by the Problem, Fix, Background, and Downsides sections. It also has the required "How did you verify your code works?" content, covered by the Verified line, the Tests section, and the Measurements section. It names the affected files and tests, reports pass and fail counts, and lists the decisions a maintainer must make. The only gap is that the Verified line points to a debug build for some results and a release build for others, and it defers the platform-specific test run to CI. This is a minor gap and does not make the description incomplete.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/runtime/shell/states/Cmd.rs Resolved
Comment thread src/runtime/shell/states/Expansion.rs Resolved
Comment thread src/runtime/shell/states/Assigns.rs Resolved
Brace expansion of an assignment value is not changed here, so the test
does not fix `a* b*` as the expected value. The `export` test with a brace
group still covers the glob start after brace expansion.

@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.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
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