@@ -1058,6 +1058,8 @@ describe('code placeholder compiler', () => {
10581058 "echo $(( $(cat <<-'EOF'\n\t{{KEY}}\n\tEOF\n) + 1 ))" ,
10591059 '[[ {{KEY}} -eq 0 ]] && echo zero' ,
10601060 '[[ "{{KEY}}" -eq 0 ]] && echo zero' ,
1061+ 'declare -i n; declare -a n; n={{KEY}}' ,
1062+ 'declare -i n; unset -f n; n={{KEY}}' ,
10611063 'if [[ 0 -lt {{KEY}} ]]; then echo positive; fi' ,
10621064 '[[ -n x && ( "{{KEY}}" -ge 1 ) ]]' ,
10631065 '[[ $(printf "%s" "{{KEY}}") -ne 0 ]]' ,
@@ -1136,7 +1138,7 @@ describe('code placeholder compiler', () => {
11361138 } )
11371139
11381140 it ( 'preserves shell literal arithmetic text and leaves completed arithmetic frames' , async ( ) => {
1139- const value = 'values[$(printf injected >&2) ]'
1141+ const value = 'values[1 ]'
11401142 const compiled = await compileCodePlaceholders ( {
11411143 code : [
11421144 'printf "%s\\n" "{{KEY}}"' ,
@@ -1180,6 +1182,7 @@ describe('code placeholder compiler', () => {
11801182 '$(let x=1 <<EOF\n{{KEY}}\nEOF\n)' ,
11811183 // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
11821184 'declare -A m; m[{{KEY}}]=x; printf "%s\\n" "${m[{{KEY}}]}"' ,
1185+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
11831186 'declare -A m; printf "%s\\n" "${m[{{KEY}}]}"' ,
11841187 'declare -iA m; m[{{KEY}}]=1' ,
11851188 'read -p "items[{{KEY}}]" name' ,
@@ -1198,7 +1201,7 @@ describe('code placeholder compiler', () => {
11981201 compileCodePlaceholders ( {
11991202 code,
12001203 language : CodeLanguage . Shell ,
1201- environmentVariables : { KEY : 'values[$(printf injected >&2) ]' } ,
1204+ environmentVariables : { KEY : 'values[1 ]' } ,
12021205 } )
12031206 ) . resolves . toBeDefined ( )
12041207 } )
@@ -1214,7 +1217,7 @@ describe('code placeholder compiler', () => {
12141217 } )
12151218
12161219 it ( 'compiles shell placeholders beside arithmetic that never evaluates them' , async ( ) => {
1217- const value = 'values[$(printf injected >&2) ]'
1220+ const value = 'values[1 ]'
12181221 const compiled = await compileCodePlaceholders ( {
12191222 code : [
12201223 '[ "{{KEY}}" -eq 0 ] 2>/dev/null || printf "%s\\n" not-zero' ,
@@ -1249,6 +1252,62 @@ describe('code placeholder compiler', () => {
12491252 )
12501253 } )
12511254
1255+ it . each ( [
1256+ [ 'an expansion in a subscript' , 'a[$(cmd)]' , 'an array subscript with an expansion' ] ,
1257+ [
1258+ 'a quoted bracket ending the walk early' ,
1259+ 'a["]$(cmd)"]' ,
1260+ 'an array subscript with an expansion' ,
1261+ ] ,
1262+ [ 'a backtick in a subscript' , 'a[`cmd`]' , 'an array subscript with an expansion' ] ,
1263+ [ 'a process substitution in a subscript' , 'a[<(cmd)]' , 'an array subscript with an expansion' ] ,
1264+ [ 'an escaped expansion in a subscript' , 'a[\\$x]' , 'an array subscript with an expansion' ] ,
1265+ [ 'an unclosed subscript' , 'a[' , 'an unbalanced array subscript' ] ,
1266+ [ 'a stray closing bracket' , '$(cmd)]' , 'an unbalanced array subscript' ] ,
1267+ [ 'an open quote in a subscript' , 'a["]' , 'an unbalanced array subscript' ] ,
1268+ ] ) ( 'refuses a shell value with %s wherever it lands' , async ( _case , value , reason ) => {
1269+ for ( const code of [ 'printf "%s\\n" "{{KEY}}"' , "cat <<'EOF'\n{{KEY}}\nEOF" ] ) {
1270+ await expect (
1271+ compileCodePlaceholders ( {
1272+ code,
1273+ language : CodeLanguage . Shell ,
1274+ environmentVariables : { KEY : value } ,
1275+ } )
1276+ ) . rejects . toThrow ( reason )
1277+ }
1278+ } )
1279+
1280+ it . each ( [ 'values[1]' , '["a", "b"]' , '{"items": [1, 2]}' , '$HOME and $(date)' , "it's [done]" ] ) (
1281+ 'compiles a shell value whose subscripts hold no expansion: %s' ,
1282+ async ( value ) => {
1283+ const compiled = await compileCodePlaceholders ( {
1284+ code : 'printf "%s\\n" "{{KEY}}"' ,
1285+ language : CodeLanguage . Shell ,
1286+ environmentVariables : { KEY : value } ,
1287+ } )
1288+ expect ( executeShell ( compiled . code , compiled . bindings ) ) . toBe ( `${ value } \n` )
1289+ }
1290+ )
1291+
1292+ it ( 'refuses arithmetic positions whose values are safe alone' , async ( ) => {
1293+ // Code can supply the brackets, or split a subscript across adjacent placeholders, so the value
1294+ // guard cannot judge these alone; the position scan refuses them.
1295+ await expect (
1296+ compileCodePlaceholders ( {
1297+ code : 'total=$(( a[{{KEY}}] ))' ,
1298+ language : CodeLanguage . Shell ,
1299+ environmentVariables : { KEY : '$(cmd)' } ,
1300+ } )
1301+ ) . rejects . toThrow ( 'is not supported in shell arithmetic' )
1302+ await expect (
1303+ compileCodePlaceholders ( {
1304+ code : 'total=$(( {{LEFT}}{{RIGHT}} ))' ,
1305+ language : CodeLanguage . Shell ,
1306+ environmentVariables : { LEFT : 'a' , RIGHT : '1' } ,
1307+ } )
1308+ ) . rejects . toThrow ( 'is not supported in shell arithmetic' )
1309+ } )
1310+
12521311 it ( 'discovers shell arithmetic placeholders without compiling missing values' , async ( ) => {
12531312 const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
12541313 await expect ( analyzeCodePlaceholders ( code , CodeLanguage . Shell ) ) . resolves . toEqual ( [
0 commit comments