@@ -33,6 +33,10 @@ interface ShellScanFrame {
3333 /** A bare name[index] is arithmetic only when its closing bracket is followed by assignment. */
3434 arrayAssignment ?: boolean
3535 arithmeticParent ?: ShellScanFrame
36+ wordStart ?: number
37+ commandPosition ?: boolean
38+ declarationCommand ?: boolean
39+ redirectionTarget ?: boolean
3640 literalRoot : boolean
3741}
3842
@@ -139,6 +143,7 @@ function readShellArrayStart(
139143 parent . quote === 'none' &&
140144 ! parent . literalRoot &&
141145 wordStart &&
146+ ( parent . kind === 'array' || parent . commandPosition !== false || parent . declarationCommand ) &&
142147 ( hasName || ( character === '[' && parent . kind === 'array' ) )
143148 if ( ! parameter && ! assignment ) return undefined
144149
@@ -282,6 +287,52 @@ function readHeredocDelimiterWord(
282287 return consumed ? { delimiter, quoted, end : cursor } : undefined
283288}
284289
290+ /** Assignment-shaped words after an ordinary command name are arguments, not arithmetic. */
291+ function trackShellCommandPosition ( code : string , index : number , frame : ShellScanFrame ) : void {
292+ if (
293+ frame . literalRoot ||
294+ frame . quote !== 'none' ||
295+ ( frame . kind !== 'root' && frame . kind !== 'command' && frame . kind !== 'backtick' )
296+ ) {
297+ return
298+ }
299+ const character = code [ index ]
300+ const redirection = character === '<' || character === '>' || code . startsWith ( '&>' , index )
301+ const separator =
302+ / [ ; \n & | ( ) ] / . test ( character ) &&
303+ ! redirection &&
304+ ! ( ( character === '&' || character === '|' ) && / [ < > ] / . test ( code [ index - 1 ] ?? '' ) )
305+ if ( ! separator && ! redirection && ! / \s / . test ( character ) ) {
306+ if ( character === '\\' && / ^ \r ? \n / . test ( code . slice ( index + 1 , index + 3 ) ) ) return
307+ frame . wordStart ??= index
308+ return
309+ }
310+
311+ if ( frame . wordStart !== undefined ) {
312+ const word = code . slice ( frame . wordStart , index ) . replace ( / \\ \r ? \n / g, '' )
313+ if ( frame . redirectionTarget ) {
314+ frame . redirectionTarget = false
315+ } else if (
316+ ! ( redirection && / ^ \d + $ / . test ( word ) ) &&
317+ frame . commandPosition !== false &&
318+ ! / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * (?: \+ ? = | \[ [ \s \S ] * \] \+ ? = ) / . test ( word ) &&
319+ ! / ^ (?: i f | t h e n | e l s e | e l i f | w h i l e | u n t i l | d o | ! | \{ | t i m e | c o m m a n d | b u i l t i n | e x e c ) $ / . test ( word )
320+ ) {
321+ frame . commandPosition = false
322+ const name = readHeredocDelimiterWord ( word , 0 , word . length ) ?. delimiter
323+ frame . declarationCommand = / ^ (?: d e c l a r e | e x p o r t | l o c a l | r e a d o n l y | t y p e s e t ) $ / . test ( name ?? '' )
324+ }
325+ frame . wordStart = undefined
326+ }
327+ if ( separator ) {
328+ frame . commandPosition = true
329+ frame . declarationCommand = false
330+ frame . redirectionTarget = false
331+ } else if ( redirection ) {
332+ frame . redirectionTarget = true
333+ }
334+ }
335+
285336function parseHeredocHeaders (
286337 code : string ,
287338 lineStart : number ,
@@ -536,6 +587,7 @@ function collectShellOccurrenceContexts(
536587 const frame = frames . at ( - 1 )
537588 if ( ! frame ) break
538589
590+ trackShellCommandPosition ( code , index , frame )
539591 const occurrence = occurrenceByStart . get ( index )
540592 if ( occurrence ) {
541593 contexts . set ( occurrence , {
@@ -614,6 +666,7 @@ function collectShellOccurrenceContexts(
614666 }
615667 if ( ! frame . literalRoot && shellCommentStarts ( code , index ) ) {
616668 const newline = code . indexOf ( '\n' , index )
669+ if ( newline !== - 1 && newline < end ) trackShellCommandPosition ( code , newline , frame )
617670 index = newline === - 1 || newline >= end ? end : newline + 1
618671 continue
619672 }
0 commit comments