@@ -1053,48 +1053,107 @@ describe('code placeholder compiler', () => {
10531053 'total=$(( $(( 1 + 1 )) + {{KEY}} ))' ,
10541054 'cat <<PAYLOAD\n$(( {{KEY}} * 2 ))\nPAYLOAD' ,
10551055 'cat <<PAYLOAD\n$[ {{KEY}} * 2 ]\nPAYLOAD' ,
1056- ] ) ( 'rejects shell placeholders whose values enter arithmetic: %s' , async ( code ) => {
1056+ // Rejection is value-based and position-independent: a value whose array subscript carries an
1057+ // expansion would run the embedded command only if it reached arithmetic, so it is refused
1058+ // wherever the placeholder lands — including ordinary, non-arithmetic positions.
1059+ 'printf "%s" "{{KEY}}"' ,
1060+ '[ "{{KEY}}" = x ]' ,
1061+ 'echo {{KEY}}' ,
1062+ 'cat <<PAYLOAD\n{{KEY}}\nPAYLOAD' ,
1063+ "cat <<'PAYLOAD'\n{{KEY}}\nPAYLOAD" ,
1064+ ] ) (
1065+ 'rejects a shell value whose array subscript contains an expansion, in any position: %s' ,
1066+ async ( code ) => {
1067+ await expect (
1068+ compileCodePlaceholders ( {
1069+ code,
1070+ language : CodeLanguage . Shell ,
1071+ environmentVariables : { KEY : 'values[$(printf injected >&2)]' } ,
1072+ } )
1073+ ) . rejects . toThrow ( 'array subscript with an expansion' )
1074+ }
1075+ )
1076+
1077+ it . each ( [
1078+ 'values[`printf injected >&2`]' ,
1079+ 'a[$(id)]' ,
1080+ 'm[$(cmd)]=x' ,
1081+ 'a[<(cmd)]' ,
1082+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell expansion inside a subscript, not a JS template
1083+ 'x[${!ref}]' ,
1084+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell expansion inside a subscript, not a JS template
1085+ 'x[${v@P}]' ,
1086+ ] ) ( 'rejects every expansion flavour inside a shell value subscript: %s' , async ( value ) => {
10571087 await expect (
10581088 compileCodePlaceholders ( {
1059- code,
1089+ code : 'printf "%s" "{{KEY}}"' ,
10601090 language : CodeLanguage . Shell ,
1061- environmentVariables : { KEY : 'values[$(printf injected >&2)]' } ,
1091+ environmentVariables : { KEY : value } ,
10621092 } )
1063- ) . rejects . toThrow ( 'is not supported in shell arithmetic ' )
1093+ ) . rejects . toThrow ( 'array subscript with an expansion ' )
10641094 } )
10651095
1066- it ( 'preserves shell literal arithmetic text and leaves completed arithmetic frames ' , async ( ) => {
1067- const value = 'values[$(printf injected >&2)] '
1096+ it ( 'preserves shell literal arithmetic text for values without a subscript expansion ' , async ( ) => {
1097+ const value = 'raw $(text) and `more` [0] value '
10681098 const compiled = await compileCodePlaceholders ( {
10691099 code : [
10701100 'printf "%s\\n" "{{KEY}}"' ,
10711101 "printf '%s\\n' '$(( {{KEY}} ))'" ,
10721102 "printf '%s\\n' '$[ {{KEY}} ]'" ,
10731103 'printf "%s\\n" "$(printf %s "{{KEY}}")"' ,
10741104 'printf "%s\\n" "$(( 1 + 1 )){{KEY}}"' ,
1075- 'printf "%s\\n" "$[ values[0] + 2 ]{{KEY}}"' ,
10761105 '(( total = 2 )); printf "%s\\n" "{{KEY}}"' ,
10771106 'cat <<PAYLOAD' ,
1078- '(( {{KEY}} )) ' ,
1107+ '{{KEY}}' ,
10791108 'PAYLOAD' ,
10801109 ] . join ( '\n' ) ,
10811110 language : CodeLanguage . Shell ,
10821111 environmentVariables : { KEY : value } ,
10831112 } )
10841113
10851114 expect ( executeShell ( compiled . code , compiled . bindings ) ) . toBe (
1086- `${ value } \n$(( ${ value } ))\n$[ ${ value } ]\n${ value } \n2${ value } \n2 ${ value } \ n${ value } \n(( ${ value } )) \n`
1115+ `${ value } \n$(( ${ value } ))\n$[ ${ value } ]\n${ value } \n2${ value } \n${ value } \n${ value } \n`
10871116 )
10881117 } )
10891118
1090- it ( 'discovers shell arithmetic placeholders without compiling missing values' , async ( ) => {
1119+ it . each ( [
1120+ 'total=$(( {{KEY}} * 2 ))' ,
1121+ '(( total = {{KEY}} ))' ,
1122+ 'total=$[ {{KEY}} ]' ,
1123+ '[[ {{KEY}} -eq 0 ]]' ,
1124+ 'let "x={{KEY}}"' ,
1125+ 'declare -i x="{{KEY}}"' ,
1126+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1127+ 's=abcdef; printf "%s\\n" "${s:{{KEY}}}"' ,
1128+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1129+ 'a=(1 2); printf "%s\\n" "${a[{{KEY}}]}"' ,
1130+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1131+ 'a=([{{KEY}}]=text); printf "%s\\n" "${a[*]}"' ,
1132+ '[[ "{{KEY}}" == "-eq" ]] && printf "%s\\n" matched' ,
1133+ 'unset "a[{{KEY}}]"' ,
1134+ ] ) ( 'compiles a safe value in any position, including arithmetic ones: %s' , async ( code ) => {
1135+ // A value with no array-subscript expansion cannot run a command in arithmetic (verified in
1136+ // bash 5.2), so it is accepted everywhere — no position detection, no over-rejection.
1137+ await expect (
1138+ compileCodePlaceholders ( {
1139+ code,
1140+ language : CodeLanguage . Shell ,
1141+ environmentVariables : { KEY : 'items[0]' } ,
1142+ } )
1143+ ) . resolves . toBeDefined ( )
1144+ } )
1145+
1146+ it ( 'discovers shell arithmetic placeholders without binding missing values' , async ( ) => {
10911147 const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
10921148 await expect ( analyzeCodePlaceholders ( code , CodeLanguage . Shell ) ) . resolves . toEqual ( [
10931149 'MISSING' ,
10941150 'KEY' ,
10951151 ] )
10961152 const compiled = await compileCodePlaceholders ( { code, language : CodeLanguage . Shell } )
1097- expect ( compiled . code ) . toBe ( code )
1153+ // With no configured values nothing is bound; unresolved placeholders expand to empty, as in
1154+ // every other position (the arithmetic position itself is no longer special).
1155+ expect ( compiled . bindings ) . toEqual ( [ ] )
1156+ expect ( compiled . code ) . toBe ( 'total=$(( + ))' )
10981157 } )
10991158
11001159 it ( 'renders quoted shell heredocs nested in double-quoted command substitutions' , async ( ) => {
0 commit comments