@@ -106,8 +106,16 @@ interface ShellCommandScan {
106106 inRedirectTarget : boolean
107107}
108108
109+ /** The variable attributes visible at a point in the scan, which a heredoc body read there inherits. */
110+ interface ShellAttributeScope {
111+ integerAttribute : Map < string , boolean >
112+ associativeArrays : Set < string >
113+ }
114+
109115interface ShellOccurrenceContext {
110116 quote : ShellQuote
117+ /** On a heredoc operator, the attributes in scope there, so its body scan sees the same arrays. */
118+ scope ?: ShellAttributeScope
111119 /** Its value reaches an arithmetic evaluation — a frame, or a command marked arithmetic later. */
112120 arithmetic ?: boolean
113121 unsupported ?: 'escaped sequence'
@@ -738,7 +746,8 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
738746 end : number ,
739747 literalRoot : boolean ,
740748 skippedRanges : Array < [ number , number ] > = [ ] ,
741- operatorStarts : ReadonlySet < number > = new Set ( )
749+ operatorStarts : ReadonlySet < number > = new Set ( ) ,
750+ inheritedScope ?: ShellAttributeScope
742751) : Map < T , ShellOccurrenceContext > {
743752 const occurrenceByStart = new Map (
744753 occurrencesWithin ( occurrences , start , end ) . map (
@@ -747,7 +756,14 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
747756 )
748757 const contexts = new Map < T , ShellOccurrenceContext > ( )
749758 const frames : ShellScanFrame [ ] = [
750- { kind : 'root' , quote : 'none' , parenthesisDepth : 0 , literalRoot } ,
759+ {
760+ kind : 'root' ,
761+ quote : 'none' ,
762+ parenthesisDepth : 0 ,
763+ literalRoot,
764+ integerAttribute : new Map ( inheritedScope ?. integerAttribute ) ,
765+ associativeArrays : new Set ( inheritedScope ?. associativeArrays ) ,
766+ } ,
751767 ]
752768 let skippedRangeIndex = 0
753769 /**
@@ -791,6 +807,17 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
791807 }
792808 return false
793809 }
810+ /** The attributes visible here, outermost scope first so a nearer one overrides an integer flag. */
811+ const visibleScope = ( ) : ShellAttributeScope => {
812+ const scope : ShellAttributeScope = { integerAttribute : new Map ( ) , associativeArrays : new Set ( ) }
813+ for ( const frame of frames ) {
814+ for ( const [ name , attribute ] of frame . integerAttribute ?? [ ] ) {
815+ scope . integerAttribute . set ( name , attribute )
816+ }
817+ for ( const name of frame . associativeArrays ?? [ ] ) scope . associativeArrays . add ( name )
818+ }
819+ return scope
820+ }
794821 /** Whether `name` is a `declare -A` associative array, whose subscript is a string key, not arithmetic. */
795822 const isAssociativeArray = ( name : string ) =>
796823 frames . some ( ( frame ) => frame . associativeArrays ?. has ( name ) )
@@ -1012,6 +1039,7 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
10121039 const operatorContext : ShellOccurrenceContext = {
10131040 quote : 'none' ,
10141041 arithmetic : enclosingArithmetic ,
1042+ scope : visibleScope ( ) ,
10151043 }
10161044 contexts . set ( occurrence , operatorContext )
10171045 if ( sub >= 0 ) commandScanOf ( frames [ sub - 1 ] ) . contexts . push ( operatorContext )
@@ -1034,10 +1062,15 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
10341062 }
10351063
10361064 const character = code [ index ]
1037- // The redirect-target flag covers only its one word; unquoted whitespace ends that word, so a
1038- // following argument (`let x=1 >/dev/null {{x}}`) is marked by the command again.
1039- if ( frame . command ?. inRedirectTarget && frame . quote === 'none' && / \s / . test ( character ) ) {
1065+ // The redirect-target and assignment-value flags cover only their one word; unquoted whitespace
1066+ // ends that word. `scanWord` clears them at the next word too, but a placeholder-only word never
1067+ // reaches it, so `let x=1 >/dev/null {{x}}` is marked by the command again while the command name
1068+ // in `n=1 {{x}} arg` is not read as `n`'s integer value.
1069+ if ( frame . command && frame . quote === 'none' && / \s / . test ( character ) ) {
10401070 frame . command . inRedirectTarget = false
1071+ frame . command . valueArithmetic = false
1072+ frame . command . pendingValue = false
1073+ frame . command . inPrefixValue = false
10411074 }
10421075 // Classify each command word before the quote branches consume a quote-initial word such as
10431076 // `"-i"` or `'let'`. `scanWord` only sets state, never advances `index`; the characters below
@@ -1628,7 +1661,10 @@ export async function compileShellPlaceholders(
16281661 bodyOccurrences ,
16291662 heredoc . bodyStart ,
16301663 heredoc . bodyEnd ,
1631- true
1664+ true ,
1665+ [ ] ,
1666+ undefined ,
1667+ rootContexts . get ( heredocOperators [ heredocIndex ] ) ?. scope
16321668 )
16331669 for ( const occurrence of bodyOccurrences ) {
16341670 const edit = resolveInContext ( occurrence , bodyContexts . get ( occurrence ) , false )
0 commit comments