Repository navigation
Conversation
…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.
|
Status Reproduced on main and on 1.4.2, in a folder with the two files bun exec 'DEBUG=* printenv DEBUG'
bun exec 'export DEBUG=xss:* && printenv DEBUG'
The Notes of the description list two decisions for a maintainer: the release that takes the silent change, and the glob of a bare CI: build 136940 is complete. |
|
No actionable comments were generated in the recent review.
|
| 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: 
4
-
Full details: Title check
-
Explanation
The title names the main change: assignment values in
NAME=value cmdandexport NAME=valueno 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.
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.
Problem
DEBUG=* node app.jsgives the child file names.export DEBUG=xss:* && mochafails:bun: no matches found: DEBUG=xss:*.Expansion(src/runtime/shell/states/Expansion.rs:249,:344) ignore what the word is for.DEBUG=* bun foo.jsgives*. It was not added.Fix
Expansion::inittakes aWordContext, not theassign_ctxbool. Both glob starts skip anEnvValueword (NAME=value cmd,export NAME=value): no glob task, no folder read.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 oftest/js/bun/shell/. Self-reviewed: 12 concerns raised, 11 addressed (Notes).Background
Expansionturns one word into arguments. A literal*starts aShellGlobTask: a folder walk on the work pool.assign_ctxfor the rule's other half: no field split of$(...)in these values.exportoperand has noAssignsparent) and a parser rewrite (every parsed command pays).Downsides
F=dist/*.tgz cmdstill exits 0, butcmdgets the text. All 30 such words in 63,428 npm scripts are written for sh. Notes asks which release takes it.NAME=valueglobs,FOO={a,b}* cmdgivesa* b*,DEBUG=** cmdfails, and a node child keeps an inheritedDEBUG(Bun Shell: prefix assignment of an existing env var duplicates the environ entry (child sees the old value) #32202)..textis unchanged.Notes
Two decisions for a maintainer
mainis 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.NAME=value. This PR keeps it, because a landed test pinstouch hihello; touch hifriends; FOO=hi*; echo $FOOto the two names (bunshell.test.ts, "Trailing asterisk with matches, inline"). The facts:Assigns::child_done). So onlyecho $FOO, or a pattern with one match, gives what sh gives.FOO=hi*; ls $FOOfails on main and here:ls: hihello hix: No such file or directory.FOO=hi*; echo "$FOO"printshi*as in sh, andFOO=hi*; echo $FOOchanges from the names tohi*.*: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.tgzand a file namedFOO=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:
DEBUG=* printenv DEBUG**F=dist/*.tgz printenv Fdist/pkg.tgzdist/*.tgzdist/*.tgzFOO=*$(echo x) printenv FOOFOO=hix hix*x*xFOO={a,b}* printenv FOOa* b* a1 b1a* b*{a,b}*export FOO=hi* && printenv FOOhix(the wordFOO=hi*matches the fileFOO=hix)hi*hi*DEBUG=* true; printenv DEBUG*Commands that fail on 1.4.x and work now:
export DEBUG=xss:* && printenv DEBUGbun: no matches found: DEBUG=xss:*, exit 1xss:*xss:*export FOO={a,b}* && printenv FOObun: no matches found: FOO={a,b}*, exit 1b*b*(dash:{a,b}*)Not changed:
FOO=hi*; echo $FOOhihello hixhihello hixFOO=hi*; echo "$FOO"hihello hixhi*FOO=hi*; ls $FOOls: hihello hix: No such file or directoryFOO={a,b} printenv FOOa b{a,b}echo FOO=hi*FOO=hixFOO=hixDEBUG=** printenv DEBUGbun: command not found: DEBUG**FOO=~/x printenv FOO~/x$HOME/xbun run --shell=bun devwith"dev": "DEBUG=* bun foo.js"printsfoo.js package.jsonon main and*here. Bun Shell runs package.json scripts by default on Windows.F=$(echo dist/*.tgz) cmd.Not changed, on purpose
The list is for the fixture above.
NAME=valuekeeps its glob (decision 2).*of each variant stays text now (a* b*). No test expects that value.[[ ]]operands expand as on main.cross-env DEBUG=* cmd,env DEBUG=* cmd,echo DEBUG=*. With no matching file these fail withno matches found(run-pdoesn't work with wildcards #10581, open shell: pass a glob with no match to the command as written #43746).**takes the next word with it. That is a lexer defect on main (open shell: keep a digit in its word before>or<, end the word after**, and reject<<#44532).DEBUG=** cmdfails as before.export A=x** B=1gaveno matches found: A=x**B=1and now setsAtox**B=1.~after=is not expanded, in all three forms.export F=dist/*.tgz && ls $Fstill fails. main fails at theexport. This PR fails atls, which gets the text, because a value is not matched where it is used.DEBUGalready in the environment, a child gets twoDEBUG=entries (Bun Shell: prefix assignment of an existing env var duplicates the environ entry (child sees the old value) #32202). node reads the first one, which is the old value. bun and sh read the last one.exportoperand is the one of shell: stop field splitting command substitution in export operands and assignments #40764: the literal commandexportand a word that starts with a literalNAME=. It does not see quotes. Soexport "FOO="hi*and"export" FOO=hi*beside a fileFOO=hixnow givehi*. main and bash givehixfor both.export FOO={a,b}*beside a file namedFOO=bz: main and bash givebz, this PR givesb*.Who is affected
exporthave an unquoted*: 28DEBUG, 1NODE_DEBUG, 1MARKDOWN_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).npm runandnode --run(both through sh) and cross-env 10.1.0 pass the text.@yarnpkg/shell4.1.3 anddax-sh0.44.1 glob these values, as Bun does on main.DEBUG='*' cmdtoday. 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_eventreader forinstructions:uandbranches:u, aptracesyscall counter, and gdb hit counts on the mimalloc entry points. valgrind, perf and strace are not on the machine.BUN_DEBUG_Glob):DEBUG=* true,export FOO=hi*,FOO={a,b}* true,export FOO={a,b}*andFOO=*$(echo x) truego from 1 to 0. A bareFOO=hi*,echo one*,echo hi > o*.txtand[[ -f o*.txt ]]stay at 1.bun exec 'DEBUG=* true': 200 to 213 syscalls and 3 threads on main, 132 and 0 here. The quotedDEBUG='*' trueis 132 and 0 on both.A=* truestatement: 44 on main, 16 here. The quoted form is 15 on both. Pool tasks: 1 on main, 0 here.A=* truein 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 cis 4.3 ms and 4.6 ms.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 ofA=1 truecost 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.jmp: LLVM now places the exit of a one-atom word with no*behind it.*pays 2 instructions (cmpb,jne) before each of the two glob starts.Interpreter::next_node(it holdsExpansion::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_threadandInterpreter::child_donedo not change..textis 140,286,972 bytes on both.size_of:WordContext1 (the bool was 1),Expansion168,Node176. All as on main.Tests
exporttest with a brace group. With the bare context for a value before a command: the three tests of that form. WithFieldsfor anexportoperand: the threeexporttests.test/js/bun/shell/bunshell.test.tson the debug build: 471 pass, 3 skip (they need a user that is not root), 0 fail.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 ofcommands/andtest/cli/run/run-shell.test.ts.commands/ls.test.ts: in one run,recursive > node_modulestimed out after 5000 ms. That test waits for a realbun install. A release build of main has the same timeout.Design
WordContextname, itsFieldsvariant andfield_splitting()come from open shell: expand a[[ ]]operand to one word and match*on the right of == and != #45968. The bare form isShellVarValuehere, notAssignValue: in shell: expand a[[ ]]operand to one word and match*on the right of == and != #45968AssignValuemeans all three assignment forms.Option<AssignCtx>has the same size, but anexportoperand never reachesassign_var, so it has noAssignCtx.on_glob_walk_done, after the walk, leaves the pool task and the folder read.on_glob_walk_doneis not changed. Its test of the parent node gives the same answer for a bare value.Self-review
on_glob_walk_doneis as on main again, the bare variant has a name that shell: expand a[[ ]]operand to one word and match*on the right of == and != #45968 does not use, the tables above are split by kind, and the list of what stays is longer.?and[...]written in the script again #39706 from this PR. That test expects the old value until this PR lands, so shell: expand?and[...]written in the script again #39706 gets a comment.Open PRs on the same lines
Conflict regions are from
git merge-file --diff3on the four state files.?and[...]written in the script again #39706 merges with no conflict, but its test "in the environment of a command" expects file names forFOO=*.[jt]s cmd. With this PR that value is the text*.[jt]s. The one that lands second must change that expectation.[[ ]]operand to one word and match*on the right of == and != #45968 replaces the same bool with aWordContextof four variants: 5 conflict regions on main, 7 with this branch. After this PR it can add its variants andbrace_expansion(), and putEnvValuein its arm that does not glob.${NAME}and the${NAME<op>word}forms #46691 and shell: glob each brace variant in one pool task, and keep a variant with no match #33423: 0 regions on main, 1 each with this branch. shell: glob each brace variant in one pool task, and keep a variant with no match #33423 rewrites the glob start and must keep the context test.me.assign_ctx, which is gone. The same meaning is!me.ctx.field_splitting().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