Skip to content

shell: glob brace-expanded patterns instead of dropping the glob - #30999

Merged
Jarred-Sumner merged 4 commits into
mainfrom
ali/shell-brace-glob
May 20, 2026
Merged

Jarred-Sumner merged 4 commits into
mainfrom
ali/shell-brace-glob

Conversation

@alii

@alii alii commented May 18, 2026

Copy link
Copy Markdown
Member

A shell word combining brace + glob (src/*.{ts,tsx}, {src,lib}/*.ts, rm -rf {dist,build}/*.js) was brace-expanded but the resulting * patterns were never globbed. Silent, exit 0. Affects Bun.$, --shell=bun, bunfig shell="bun", and Windows bun run.

do_brace_expand ended with me.state = if has_glob_expansion() { Done } else { Done } — both arms Done, the glob check a dead no-op (an in-source TODO admitted it). It also cleared current_out, which Zig keeps. Zig re-enters the .glob state after brace expansion, so the original pattern (e.g. src/*.{ts,tsx}) is globbed (the glob walker brace-expands and globs it) and its matches are appended after the literal brace variants.

When has_glob_expansion(), keep current_out and set state to Glob; the BraceExpand arm then dispatches transition_to_glob_state (the same call the normal glob path makes). Output is now byte-identical to released Bun: echo src/*.{ts,tsx} → src/*.ts src/*.tsx src/app.ts src/util.tsx. Regression test in brace.test.ts with a temp fixture.

@coderabbitai

coderabbitai Bot commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 976a470e-cf91-463a-a063-16073b689802

📥 Commits

Reviewing files that changed from the base of the PR and between 4c8a21b and 97f5e82.

📒 Files selected for processing (3)
  • src/jsc/bindings/wtf-bindings.cpp
  • src/runtime/shell/states/Expansion.rs
  • test/js/bun/shell/brace.test.ts

Walkthrough

Shell expansion state machine updated to compose brace expansion with glob expansion. The Expansion state now conditionally retains output when both syntaxes apply, enabling glob walking to operate on brace-glob patterns. Tests validate the composed expansion behavior with temporary file structures.

Changes

Shell brace-glob expansion composition

Layer / File(s) Summary
Brace expansion state machine composition logic
src/runtime/shell/states/Expansion.rs
State re-entry in BraceExpand conditionally transitions to glob-walking when state is Glob. Inside do_brace_expand, current_out is retained (instead of cleared) when the atom contains glob syntax, allowing the glob walker to operate on the composed brace-glob pattern.
Tests for brace-glob composition
test/js/bun/shell/brace.test.ts
New test suite with tempDir import validates composed brace-glob expansion by creating temporary directories with sample TypeScript files, running shell commands with combined patterns like {src,lib}/*.ts, and asserting both brace-expanded variants and glob matches appear in output.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: globbing brace-expanded patterns instead of dropping them.
Description check ✅ Passed The PR description is comprehensive and detailed, exceeding template requirements with clear problem statement, root cause, solution, and expected behavior.
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.

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


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

@robobun

robobun commented May 18, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 8:23 PM PT - May 18th, 2026

✅ @robobun, your commit 97f5e8288d187b2028b96b44fd93b796a28d1850 passed in Build #55939! 🎉


🧪   To try this PR locally:

bunx bun-pr 30999

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

bun-30999 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. bun shell glob match fails #18656 - Shell glob patterns are mangled/fail when combined with wildcard expansion (e.g., rm -rf packages/*/node_modules), which is the same brace+glob expansion bug fixed here

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #18656

🤖 Generated with Claude Code

Comment thread test/js/bun/shell/brace.test.ts Outdated
Comment thread test/js/bun/shell/brace.test.ts Outdated
@alii

alii commented May 18, 2026

Copy link
Copy Markdown
Member Author

@robobun fix review comments

@robobun

robobun commented May 18, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Review comments addressed:

  • 45bd924 — normalize \ → / in glob output so the brace + glob assertions pass on Windows; split on spaces and assert with array toContain so src/*.ts isn't satisfied as a substring of src/*.tsx.
  • e36ec3a — swap a stray libc assert() for WTF's ASSERT in wtf-bindings.cpp (missing <cassert> was breaking the debug build under the current unified-source grouping).

CI on 97f5e82 (#55939): every test lane that ran is green — all three Windows lanes, debian-13-x64-asan, and all Linux/Alpine/Ubuntu variants. The three red entries are macOS test jobs marked Expired (agent pool never picked them up; no tests executed). The diff is green and ready for a maintainer to merge or retrigger the macOS agents.

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

LGTM — both earlier review comments addressed in 45bd924 (Windows separator normalization + word-level assertions).

Extended reasoning...

Overview

This PR fixes a dead-code bug in the Rust shell port's word-expansion state machine (src/runtime/shell/states/Expansion.rs). Previously do_brace_expand ended with if has_glob_expansion() { Done } else { Done } — both arms identical — so a word combining brace and glob syntax (src/*.{ts,tsx}, {src,lib}/*.ts) was brace-expanded but never globbed. The fix: when has_glob_expansion(), keep current_out (the original pattern) and set state to Glob; the BraceExpand arm in next() then dispatches transition_to_glob_state — the exact same call the non-brace glob path already makes a few lines below. Two regression tests added in test/js/bun/shell/brace.test.ts using temp fixtures.

Security risks

None. This is pure shell-word expansion bringing the Rust port to parity with the existing Zig reference (which already behaves this way in released Bun). No new input surface, no auth/crypto/permissions, no untrusted-data handling beyond what the glob walker already does.

Level of scrutiny

Moderate — shell expansion affects how Bun.$ / bun run build argv. But the change is ~20 lines, mechanically replaces an admitted-TODO no-op branch with the documented Zig behavior, and reuses the existing transition_to_glob_state entry point unchanged. The state-machine flow is straightforward: do_brace_expand sets state = Glob, caller checks for that and returns transition_to_glob_state(...) (which itself returns Yield::suspended() or, on walker-init failure, sets state = Err and returns Yield::Next — both handled correctly by the existing trampoline). current_out.clear() correctly moves to the non-glob branch only, since the glob path needs the original pattern.

Other factors

I previously left two inline comments on this PR (Windows backslash separators in test assertions; vacuous substring toContain). Both were addressed in 45bd924 — tests now .replaceAll("\\\\", "/") and assert against out.split(" ") — and both threads are marked resolved. The bug-hunting system found no new issues on the updated revision. No CODEOWNERS cover these paths. Output semantics (literal brace variants followed by glob matches) match the Zig reference per the PR description, so no new design decision is being introduced.

The libuv-derived helper called assert() without including <cassert>.
This compiled by accident via transitive includes in some unified-source
groupings but fails under the current debug build. Switch to WTF's ASSERT
(already available via root.h), matching every other assertion in this
directory.

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

Both earlier review comments are addressed (path-separator normalization + word-level assertions). The state-machine change looks correct and mirrors the Zig spec, but I'll leave this for a human since it changes user-visible shell expansion semantics and Build #55885 is currently red.

Extended reasoning...

Overview

This PR fixes brace+glob composition in the Rust shell interpreter port. do_brace_expand previously had a dead if has_glob_expansion() { Done } else { Done } branch and unconditionally cleared current_out; the fix keeps current_out and transitions to ExpansionState::Glob so the BraceExpand arm in next() can dispatch transition_to_glob_state — exactly the same call the plain-glob path makes a few lines below. There is also a cosmetic assert → ASSERT swap in wtf-bindings.cpp and two new regression tests in brace.test.ts.

Prior feedback

I had flagged (1) Windows path-separator breakage in the new tests and (2) a vacuous substring assertion. Both were fixed in 45bd924 via .replaceAll("\\\\", "/") + out.split(" ") with array toContain, and the inline threads are resolved. No new issues found on re-review.

Security risks

None. No untrusted input handling, auth, or crypto is touched; the glob walker and brace expander are pre-existing and only the state transition between them changed.

Level of scrutiny

Moderate. The diff is small and the control flow is easy to trace (the Err path from MAX_BRACE_EXPANSIONS still falls through to continue → break correctly), but this is a user-visible semantics change in Bun.$ / --shell=bun / Windows bun run. A human familiar with the Zig→Rust shell port should confirm the composed output (literal brace variants plus glob matches) is the intended released-Bun behavior to preserve.

Other factors

Buildkite #55885 on 45bd924 is currently reporting failures, and the head commit e36ec3a (the ASSERT swap) hasn't been confirmed green in the thread. Worth checking whether those failures are related before merge.

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

Both of my earlier comments are addressed and I didn't find anything new — the state-machine fix looks correct and matches the Zig spec, but since this changes user-facing shell expansion semantics it's worth a quick human sanity check before merge.

Extended reasoning...

Overview

Three files: (1) src/runtime/shell/states/Expansion.rs — the core fix: do_brace_expand previously ended in a dead if has_glob_expansion() { Done } else { Done } and unconditionally cleared current_out; it now sets state = Glob and preserves current_out when the atom also has a glob, and the BraceExpand arm in next() dispatches transition_to_glob_state (mirroring the normal glob path). (2) test/js/bun/shell/brace.test.ts — two new regression tests with temp fixtures. (3) src/jsc/bindings/wtf-bindings.cpp — trivial assert → WTF ASSERT swap.

Security risks

None identified. No new untrusted-input parsing, no auth/crypto/permissions. The glob walk itself is unchanged — this just re-enters the existing glob path that was already reachable for non-brace patterns.

Level of scrutiny

Moderate. The diff is small (~15 logic lines) and replaces obviously-dead code with the documented Zig behavior, but it alters the shell interpreter's expansion state machine and changes user-visible Bun.$ output for brace+glob words (now emits literal brace variants and glob matches, which the PR states is byte-identical to released/Zig Bun). That semantic — and the interaction with on_glob_walk_done's no-match error path now that out.buf is pre-populated with brace variants — is the kind of thing a human familiar with the Zig spec should glance at.

Other factors

I previously flagged two test issues (Windows path separators, vacuous substring assertion); both were fixed in 45bd924 and the inline threads are resolved. The bug-hunting pass on the latest revision found nothing. A maintainer (alii) is already engaged on the PR. No CODEOWNERS cover these paths. I traced the new control flow: Err from the brace-cap still breaks correctly, Done still breaks, and Glob correctly bypasses the suspended-arm by dispatching directly — so the change reads as correct to me, just not trivial enough to rubber-stamp.

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.

3 participants