Skip to content

shell: expand $RANDOM, $UID, $PPID, $SECONDS, $IFS, $OSTYPE and friends - #34905

Open
robobun wants to merge 7 commits into
mainfrom
farm/10d2e500/shell-dynamic-special-vars
Open

shell: expand $RANDOM, $UID, $PPID, $SECONDS, $IFS, $OSTYPE and friends#34905
robobun wants to merge 7 commits into
mainfrom
farm/10d2e500/shell-dynamic-special-vars

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

Bash's dynamic and auto-initialized shell variables are looked up as ordinary unset environment variables in Bun Shell and silently expand to the empty string:

import { $ } from "bun";
await $`${{ raw: `n=tmp.$RANDOM.$RANDOM; printf '[%s]' "$n"` }}`.text();
// bun : "[tmp..]"            <- same name on every run
// bash: "[tmp.23685.12348]"

await $`${{ raw: `printf '[%s][%s][%s]' "$RANDOM" "$UID" "$IFS"` }}`.text();
// bun : "[][][]"
// bash: "[29413][0][ \t\n]"

So the tmp.$RANDOM.$RANDOM uniqueness idiom collides on every run, [ "$UID" = 0 ] root checks are always false, $SECONDS timing is empty, and so on. (This is the named-variable half of the special-parameter gap; $? / $$ / $# are being handled separately in #33274.)

Cause

Expansion::expand_simple_no_io resolves SimpleAtom::Var as shell_env.get(label) → export_env.get(label) → "" with no special-name fallback, so any name not present in the environment becomes the empty string.

Fix

After both env-map lookups miss, fall through to a new ShellExecEnv::append_special_var that computes the value for the known special names. A user assignment still shadows the fallback because it lands in shell_env/export_env, which are consulted first.

Variable Value
$RANDOM fresh 15-bit integer from bun_core::fast_random on every reference
$SECONDS whole seconds since the interpreter was constructed; start_instant is carried into subshells so $(... $SECONDS ...) sees the parent's elapsed time, matching bash
$IFS the POSIX default " \t\n"
$SHLVL 1 when not inherited from the environment
$LINENO 0 (source positions are not tracked through expansion)
$OSTYPE target-derived string (linux-gnu, linux-musl, darwin, msys, ...)
$HOSTNAME gethostname(2) / uv_os_gethostname
$PPID getppid(2) / uv_os_getppid
$UID / $EUID / $GROUPS getuid / geteuid / getgid (POSIX only)

Verification

New special shell variables describe block in test/js/bun/shell/bunshell.test.ts strips every name under test from the inherited environment and asserts the fallback values, including that $RANDOM differs across references, that the tmp.$RANDOM.$RANDOM idiom yields distinct names across runs, that $HOSTNAME matches os.hostname() and $PPID matches process.ppid, and that a user assignment shadows the fallback. 10 of the 12 new tests fail on the released binary and all pass on this change; the full bunshell.test.ts suite stays green.


no test proof · iteration 2 · 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

Bash's dynamic / auto-initialized shell variables ($RANDOM, $UID,
$EUID, $GROUPS, $PPID, $SECONDS, $LINENO, $IFS, $SHLVL,
$HOSTNAME, $OSTYPE) were being looked up as ordinary unset
environment variables and silently expanding to the empty string. The
ubiquitous temp-name idiom 'n=tmp.$RANDOM.$RANDOM' therefore yielded
the constant 'tmp..' on every run, and '[ "$UID" = 0 ]' root checks
were always false.

Add a fallback in variable expansion: when a $NAME misses both
shell_env and export_env, ShellExecEnv::append_special_var computes
the value for the known special names. A user assignment (which lands
in one of the env maps) still shadows the fallback.

$RANDOM draws a fresh 15-bit value from bun_core::fast_random on
every reference; $SECONDS reports whole seconds since the interpreter
was constructed (start_instant is carried into subshells so a subshell
sees the parent's elapsed time, matching bash); $UID/$EUID/$GROUPS
use getuid/geteuid/getgid on POSIX; $PPID uses getppid/uv_os_getppid;
$HOSTNAME uses gethostname/uv_os_gethostname; $OSTYPE is the
target-triple-derived string bash uses; $IFS is the POSIX default
' \t\n'; $SHLVL defaults to 1; $LINENO is 0 (source positions are
not tracked through expansion).
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Shell environments preserve interpreter startup time and expand Bash-like dynamic, static, and platform-specific variables when environment lookups do not provide values. Windows hostname retrieval is centralized, and shell tests cover defaults, overrides, and platform-specific variables.

Special shell variables

Layer / File(s) Summary
Shell environment timing
src/runtime/shell/interpreter.rs
ShellExecEnv records interpreter startup time and preserves it across subshell and pipeline duplication.
Special variable expansion
src/runtime/shell/interpreter.rs, src/runtime/shell/states/Expansion.rs
Variable expansion falls back to Bash-like dynamic, static, and platform-specific variables when no environment value exists.
Hostname retrieval integration
src/sys/windows/mod.rs, src/runtime/node/node_os.rs
Windows hostname retrieval uses a shared UTF-16 helper for Node and shell hostname expansion.
Special variable validation
test/js/bun/shell/bunshell.test.ts
Tests cover special-variable values, platform-specific identifiers, assignment overrides, and unknown variables.

Possibly related PRs

  • oven-sh/bun#34903: Both changes extend ShellExecEnv state and propagate inherited fields through subshell duplication.

Suggested reviewers: jarred-sumner, dylan-conway

🚥 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 summarizes the main change: adding Bash-like shell special-variable expansion.
Description check ✅ Passed The description covers the problem, cause, fix, and verification, which satisfies the template's intent.

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

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:45 AM PT - Jul 21st, 2026

@robobun, your commit 2991dce has 2 failures in Build #76811 (All Failures):

  • test/js/bun/http/bun-server.test.ts - code 1 on 🍎 14 aarch64
  • 📦 Binary size — 8 over 0.50 MB
  • targetthis build canary: main #76791
    sizeΔ
    bun-darwin-aarch6458.31 MB57.56 MB+759.6 KB
    bun-darwin-x6464.72 MB62.95 MB+1.77 MB
    bun-linux-aarch6470.30 MB70.74 MB-449.0 KB
    bun-linux-x6473.73 MB72.28 MB+1.45 MB
    bun-linux-x64-baseline72.67 MB
    bun-linux-aarch64-musl63.73 MB64.26 MB-537.4 KB
    bun-linux-x64-musl67.72 MB66.38 MB+1.33 MB
    bun-linux-x64-musl-baseline66.97 MB
    bun-linux-aarch64-android78.57 MB78.09 MB+491.6 KB
    bun-linux-x64-android81.55 MB80.28 MB+1.28 MB
    bun-freebsd-x6484.32 MB82.54 MB+1.78 MB
    bun-freebsd-aarch6485.01 MB84.28 MB+750.9 KB
    bun-windows-x6476.39 MB79.67 MB-3.28 MB
    bun-windows-x64-baseline75.39 MB
    bun-windows-aarch6470.85 MB70.29 MB+577.5 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 34905

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

bun-34905 --bun

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

No bugs found, but this adds user-facing shell semantics (11 new special variables with bash-compat value choices like $OSTYPEmsys on Windows, $GROUPSgetgid(), $LINENO0), so it warrants a maintainer sign-off on the API surface.

What was reviewed

  • Fallback ordering: user assignments in shell_env/export_env are consulted first, so RANDOM=x still shadows the dynamic value; unknown vars still expand to "".
  • start_instant is set at both ShellExecEnv construction sites and copied (not reset) in dupe_for_subshell.
  • Helper existence: bun_core::fast_random, bun_core::slice_to_nul, bun_sys::c::getuid/geteuid/getgid, bun_sys::posix::gethostname, and bun_libuv_sys::uv_os_getppid/gethostname all resolve; bun_libuv_sys is already a dep of the runtime crate.
  • Test flakiness: the $RANDOM uniqueness assertions have collision probability ≈2⁻³⁰.
Extended reasoning...

Overview

Adds a fallback in Expansion::expand_simple_no_io for SimpleAtom::Var: after shell_env and export_env both miss, ShellExecEnv::append_special_var computes bash's dynamic/auto-initialized variables ($RANDOM, $SECONDS, $IFS, $SHLVL, $LINENO, $OSTYPE, $HOSTNAME, $PPID, and on POSIX $UID/$EUID/$GROUPS). A new start_instant: Instant field on ShellExecEnv backs $SECONDS and is copied into subshells. 12 new tests in bunshell.test.ts strip the names from the inherited env and assert the fallback values.

Security risks

None identified. The values come from process-local syscalls (getuid, getppid, gethostname) and a non-crypto PRNG that bash's $RANDOM also uses. No untrusted input flows into buffer sizing; the hostname buffers are fixed-size stack arrays with the OS writing at most len bytes.

Level of scrutiny

Medium-high. The implementation itself is straightforward and localized (a single match arm plus one new method), but it is new user-facing API surface for Bun Shell with several bash-compat judgment calls: $OSTYPE values (darwin without a version suffix, msys on Windows), $GROUPS returning getgid() rather than the getgroups() array, $LINENO stubbed to 0, and $IFS being reported as the POSIX default even though Bun Shell's field-splitting is hardcoded. These are all defensible defaults, but per the repo's API-design guidance a maintainer should confirm them before they become observable behavior scripts depend on.

Other factors

  • Only two ShellExecEnv struct-literal sites exist (Interpreter::new and dupe_for_subshell); both received start_instant, so there's no missing initializer.
  • Only one SimpleAtom::Var expansion site exists in the shell states, so there's no sibling path that misses the fallback.
  • Tests look non-flaky (the $RANDOM distinct-draws assertion has ~2⁻³⁰ collision probability) and correctly strip inherited env entries so the fallback path is what's exercised. The $UID/$EUID/$GROUPS test is POSIX-gated to match the #[cfg(unix)] arms.
  • No prior human review on the timeline; CI is still building.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rationale for the bash-compat choices flagged above, for whoever signs off:

  • $GROUPSgetgid(): bash's GROUPS is an array whose first element is the primary GID; a scalar $GROUPS reference (which is all the Bun Shell lexer can produce today) expands to that first element. So this matches what echo $GROUPS prints in bash. The full getgroups(2) list would only be observable via ${GROUPS[@]}, which isn't supported.
  • $OSTYPEmsys on Windows: there is no native-Windows bash, so the only values scripts ever test against on Windows are msys*/cygwin* (Git Bash reports msys). Picked msys so the common case $OSTYPE in msys*|cygwin*) idiom takes the Windows branch. darwin on macOS is likewise the prefix scripts match (darwin*), without committing to a fake darwin23 version suffix.
  • $LINENO0: the expansion state has no source position today; threading one through ast::SimpleAtom is a larger change. Reporting a number (rather than the previous empty string) at least keeps [ "$LINENO" -gt 0 ]-style arithmetic from erroring. Happy to drop this one from the set if a stub value is worse than empty.
  • $IFS" \t\n": this is the value every POSIX shell reports when IFS is unset, and it is what shell: honor IFS when field-splitting command substitutions #33552 reads when it starts honoring IFS for field splitting, so the two compose.

All of these only apply when the name is absent from both shell_env and export_env; an inherited or user-assigned value wins.

uv_os_gethostname was returning an error on the Windows CI lanes,
leaving $HOSTNAME empty. Use the same GetHostNameW-then-WSAStartup
retry path that node_os::hostname uses, which is what os.hostname()
(and therefore the test's expected value) runs.

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

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/js/bun/shell/bunshell.test.ts`:
- Around line 1392-1394: Strengthen the assertion in the "$LINENO expands to a
number" test to require the documented exact output "0" instead of matching any
digit string. Keep the existing command execution and environment setup
unchanged.
🪄 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: bbd88378-786a-4fcc-820a-2cd183c19f86

📥 Commits

Reviewing files that changed from the base of the PR and between 5b98630 and 3001ea1.

📒 Files selected for processing (3)
  • src/runtime/shell/interpreter.rs
  • src/runtime/shell/states/Expansion.rs
  • test/js/bun/shell/bunshell.test.ts

Comment thread test/js/bun/shell/bunshell.test.ts
Comment thread src/runtime/shell/interpreter.rs Outdated
Comment thread src/runtime/shell/interpreter.rs Outdated
robobun and others added 2 commits July 21, 2026 09:10
…into bun_sys

convert_utf16_to_utf8_append requires the caller to pre-reserve the
simdutf output length; it was being handed Expansion::current_out,
which for a bare $HOSTNAME reference is a zero-capacity Vec, so
simdutf wrote the hostname to the 0x1 dangling spare pointer and
segfaulted on all three Windows CI lanes.

Move the GetHostNameW/WSAStartup retry into bun_sys::windows::gethostname_w
and call it from both the shell fallback and node_os::hostname so the
winsock dance has one implementation, and transcode with the reserving
to_utf8_append_to_list instead of the raw append helper.

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

Actionable comments posted: 3

🤖 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 `@src/runtime/shell/interpreter.rs`:
- Around line 2256-2257: Update the hostname expansion logic around
bun_sys::windows::gethostname_w so that a None result appends the same "unknown"
fallback used by node_os.rs. Preserve the existing UTF-8 hostname handling when
retrieval succeeds, ensuring $HOSTNAME always produces a value.

In `@src/sys/windows/mod.rs`:
- Around line 3443-3448: The GetHostNameW retry in the surrounding hostname
lookup must only run when WSAGetLastError() reports WSANOTINITIALISED. Track
whether WSAStartup succeeded, perform the retry only in that case, and call
WSACleanup after a successful startup regardless of the retry result; preserve
the existing behavior for other GetHostNameW failures.
- Around line 3439-3440: Update the GetHostNameW buffer-capacity handling around
the existing debug_assert: return None when buf.len() is less than 2, then
convert the full buf.len() to c_int with try_from and propagate conversion
failure via ?. Remove the -1 subtraction while preserving the existing hostname
retrieval flow.
🪄 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: ac181139-3dc4-49ea-b59c-5eb9ae2c1906

📥 Commits

Reviewing files that changed from the base of the PR and between 3001ea1 and 3109c7f.

📒 Files selected for processing (3)
  • src/runtime/node/node_os.rs
  • src/runtime/shell/interpreter.rs
  • src/sys/windows/mod.rs

Comment thread src/runtime/shell/interpreter.rs
Comment thread src/sys/windows/mod.rs Outdated
Comment thread src/sys/windows/mod.rs
namelen counts WCHARs including the NUL, and MSDN bounds the result at
256. The -1 reserved slot was carried over from the pre-extraction
node_os.rs code and made a 256-element buffer report WSAEFAULT on a
256-character hostname.
Comment thread src/sys/windows/mod.rs Outdated
Comment thread test/js/bun/shell/bunshell.test.ts Outdated
Comment thread src/runtime/shell/interpreter.rs Outdated
The -> bool on append_special_var was never read by the sole caller;
drop it rather than documenting a contract nothing observes.

The $SECONDS test asserted .toBe("0"), i.e. that <1s of wall clock
elapses between Instant::now() and expansion. Nothing structurally
guarantees that on a loaded debug+ASAN runner, so assert the durable
contract (non-negative integer, bounded small) instead.
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI status as of 2991dce (build 76811, finished):

  • test/js/bun/shell/bunshell.test.ts passes on every lane (including all three Windows lanes that were segfaulting at 3001ea1). Nothing touching shell/, node_os, or gethostname appears in the failures.
  • binary-size reports ±MB swings in both directions (darwin-x64 +1.77 MB, windows-x64 −3.28 MB, linux-aarch64 −449 KB) against main #76791, which is the ThinLTO / new-toolchain / WebKit-bump rework (ci: single arm64 debian-13 build host; ThinLTO everywhere; baseline-only x64; rust+link merge; sysroots; WebKit a36c188; rust 2026-07-20 #34782). This diff is ~96 net lines of Rust; the deltas are that rework settling in, not this change.
  • test/js/bun/http/bun-server.test.ts went red once on darwin-14-aarch64: the "should not use 100% CPU when websocket is idle" assertion measured 5.05% idle CPU on a loaded runner. This diff touches nothing in the HTTP server or event loop; reported separately for triage.
  • Remaining failures are unrelated flakes that passed on retry (bun-install-registry, fetch-leak, s3, require-cache, bun-upgrade, no-orphans, 20144).

Ready for review.

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

No issues found — the earlier nits (dead -> bool return, $SECONDS timing assertion, gethostname_w doc/namelen) are all addressed. Deferring to a human for sign-off since this adds user-facing shell semantics (11 new special variables with bash-compat value choices like $OSTYPE="msys" and the $LINENO=0 placeholder) and refactors the shipping Windows os.hostname() path.

What was reviewed:

  • Env-map lookup order in expand_simple_no_io — user assignments still shadow the fallback; unknown names still expand to "".
  • start_instant is threaded through both the Interpreter::init constructor and dupe_for_subshell, so $SECONDS in a subshell sees the parent's clock.
  • The node_os::hostname Windows refactor is behavior-preserving modulo the 130→256 buffer bump (which is the MSDN-documented size); slice_to_nul_u16 is correctly deleted with its last caller.
  • Test env strips every name under test from bunEnv so the fallback path is what's exercised; the $RANDOM collision assertions have ~2⁻³⁰ false-positive probability.
Extended reasoning...

Overview

Adds a fallback in Bun Shell's variable expansion for bash's dynamic/auto-initialized variables ($RANDOM, $SECONDS, $IFS, $SHLVL, $LINENO, $OSTYPE, $HOSTNAME, $PPID, and on POSIX $UID/$EUID/$GROUPS). The fallback runs only after both shell_env and export_env miss, so user assignments continue to shadow it. Also extracts the Windows GetHostNameW + WSAStartup-retry sequence from node_os::hostname into a shared bun_sys::windows::gethostname_w helper (now used by both os.hostname() and $HOSTNAME), and adds a start_instant: Instant field to ShellExecEnv for $SECONDS. ~130 net lines of Rust across four files plus a 12-test describe block.

Security risks

None identified. The new code only reads process/host metadata via existing bun_sys::c/bun_sys::posix/libuv wrappers (getuid, geteuid, getgid, getppid, gethostname, uv_os_getppid) and bun_core::fast_random. No user-controlled input reaches a syscall, allocation size, or path; the match on label is a fixed byte-string dispatch.

Level of scrutiny

Medium-high. This is a real feature touching the shell interpreter's core variable-expansion path and changing user-visible semantics for 11 variable names, plus a refactor of shipping Windows node:os code. It's not a mechanical change — there are bash-compat design choices a maintainer should ratify (e.g. $OSTYPE="msys" on Windows vs. cygwin/win32; $GROUPS = getgid() only rather than the full supplementary-group array; $LINENO=0 as a placeholder). The implementation itself is straightforward and well-tested, but per the approval bar this is beyond "simple, mechanical, or obvious".

Other factors

All prior review threads are resolved: CodeRabbit's namelen off-by-one was fixed in a8e3a28; its WSACleanup and "unknown"-fallback suggestions were withdrawn as intentional behavior-preservation; my own three nits (doc-comment wording, dead return type, $SECONDS flake bound) were applied in 2d04246 and 2991dce. The bug-hunting pass on the current head found nothing. robobun reports the full bunshell.test.ts suite green on all lanes including Windows. Test coverage is thorough — every new variable is asserted, the shadowing contract is tested, and the tmp.$RANDOM.$RANDOM motivating idiom is covered directly.

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