SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

shell: wrap only component-leading `!` when neutralizing glob metachars (#31272)

### Problem

Since #31220, glob metacharacters arriving in a shell word via `${...}`
interpolation (or `$var`, command substitution, quoted text) are
neutralized before the word reaches the glob walker. `!` is neutralized
by wrapping it in a one-branch brace group `{!}`.

The glob matcher keeps brace state in a `BoundedArray<Brace, 10>`, and
sequential brace groups occupy the stack simultaneously (a group's entry
is only popped after the recursion that matches the rest of the pattern
returns). A run of N interpolated `!` bytes therefore needs N
brace-stack slots, and at 11+ the push fails and the whole pattern is
reported as a non-match:

```js
import { $ } from "bun";
await Bun.write("prefix!!!!!!!!!!!x.txt", "");
await $`echo prefix${"!".repeat(11)}*`;
// bun: no matches found: prefix!!!!!!!!!!!*   (the file exists)
```

Before #31220 a non-leading `!` reached the matcher bare and matched
literally, so this case is a regression.

### Fix

Only a `!` at the start of a path component can act as pattern syntax
(the matcher's negation loop). Everywhere else the matcher already
treats `!` as a literal byte — it can never be the first character of a
`[...]` class either, because every `[` coming from data is itself
neutralized to `[[]`.

`neutralize_glob_metachars` now wraps `!` as `{!}` only when it starts a
component (the pattern is empty or the previously emitted byte is a
native path separator, mirroring the split `build_pattern_components`
performs) and emits every other `!` bare. A run of interpolated `!` now
costs at most one brace-stack slot per component.

### Verification

Two new cases in the existing "interpolated values cannot inject glob
syntax" block in `test/js/bun/shell/bunshell.test.ts`: an 11-`!`
interpolated run in the middle of a word and at the start of a word must
still match files containing those bytes literally.

- Without the fix both fail: `bun: no matches found: prefix!!!!!!!!!!!*`
/ `bun: no matches found: !!!!!!!!!!!keep*`
- With the fix they pass, alongside the rest of the glob-expansion
block, `test/js/bun/shell/brace.test.ts`, and
`test/js/bun/glob/{match,scan,path-length,proto}.test.ts`.
R
robobun committed
f161e0311d56ece228d71de12b7747f9c2591303
Parent: d110499
Committed by GitHub <noreply@github.com> on 5/23/2026, 8:16:38 AM