@@ -113,12 +113,14 @@ export function splitChainedCommand(command: string): string[] {
113113 i ++ ;
114114 continue ;
115115 }
116- // `&` participates in a redirect when it opens a bash combined redirect
117- // (`&>file`) or duplicates a fd after `>`/`<` (`2>&1`, `<&-`). In those
118- // positions it is not a background operator and must not split the chain —
119- // otherwise `bun run build 2>&1` fragments into a real command and a stray
120- // `1`, and the operator gets a separate approval prompt for "1".
121- if ( ch === "&" && isRedirectAmpersand ( next ) ) {
116+ // `&` is redirect-bound only by its neighbours: a preceding `>`/`<`
117+ // (fd duplication: `2>&1`, `>&2`, `<&-`) or an immediately following `>`
118+ // (combined redirect: `&>file`, `&>>file`). Any other `&` — including one
119+ // with no trailing space (`a &b`) — is the background operator and must
120+ // split the chain; otherwise `bun run build 2>&1` fragments into a real
121+ // command and a stray `1`, and the operator gets a separate approval
122+ // prompt for "1".
123+ if ( ch === "&" && isRedirectAmpersand ( previousNonSpace ( current ) , next ) ) {
122124 current += ch ;
123125 continue ;
124126 }
@@ -215,9 +217,22 @@ function unwrapGroup(segment: string): string | null {
215217 return null ;
216218}
217219
218- // `&` is the background operator when it stands alone as a word — followed by
219- // whitespace or end of input. Anywhere else it is part of a redirect token:
220- // `2>&1`, `<&-`, `&>file`.
221- function isRedirectAmpersand ( next : string | undefined ) : boolean {
222- return ! ( next === undefined || next === " " || next === "\t" ) ;
220+ // The closest non-space character already scanned into the current segment,
221+ // or undefined at the start of a segment. `&` consults this (not the
222+ // following character) to decide whether it is redirect-bound.
223+ function previousNonSpace ( current : string ) : string | undefined {
224+ const trimmed = current . trimEnd ( ) ;
225+ return trimmed . length > 0 ? trimmed [ trimmed . length - 1 ] : undefined ;
226+ }
227+
228+ // `&` is redirect-bound only when the previous non-space character is `>` or
229+ // `<` (fd duplication or close: `2>&1`, `>&2`, `<&-`), or when `&` is
230+ // immediately followed by `>` (combined redirect: `&>file`, `&>>file`).
231+ // Everything else is the background operator — a chain boundary.
232+ function isRedirectAmpersand (
233+ prev : string | undefined ,
234+ next : string | undefined ,
235+ ) : boolean {
236+ if ( next === ">" ) return true ;
237+ return prev === ">" || prev === "<" ;
223238}
0 commit comments