@@ -1053,87 +1053,6 @@ describe('code placeholder compiler', () => {
10531053 'total=$(( $(( 1 + 1 )) + {{KEY}} ))' ,
10541054 'cat <<PAYLOAD\n$(( {{KEY}} * 2 ))\nPAYLOAD' ,
10551055 'cat <<PAYLOAD\n$[ {{KEY}} * 2 ]\nPAYLOAD' ,
1056- 'echo $(( $(cat <<EOF\n{{KEY}}\nEOF\n) + 1 ))' ,
1057- "echo $(( $(cat <<'EOF'\n{{KEY}}\nEOF\n) + 1 ))" ,
1058- "echo $(( $(cat <<-'EOF'\n\t{{KEY}}\n\tEOF\n) + 1 ))" ,
1059- '[[ {{KEY}} -eq 0 ]] && echo zero' ,
1060- '[[ "{{KEY}}" -eq 0 ]] && echo zero' ,
1061- 'declare -i n; declare -a n; n={{KEY}}' ,
1062- 'declare -i n; unset -f n; n={{KEY}}' ,
1063- 'declare -i n; n=1\\ {{KEY}}' ,
1064- 'declare -i n; cat <<PAYLOAD\n$(n={{KEY}})\nPAYLOAD' ,
1065- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1066- 'declare -A m <<PAYLOAD\n${m[{{KEY}}]}\nPAYLOAD' ,
1067- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1068- 'declare -A m "${m[{{KEY}}]}"' ,
1069- 'declare -i n; declare +i n <<PAYLOAD\n$(n={{KEY}})\nPAYLOAD' ,
1070- 'if [[ 0 -lt {{KEY}} ]]; then echo positive; fi' ,
1071- '[[ -n x && ( "{{KEY}}" -ge 1 ) ]]' ,
1072- '[[ $(printf "%s" "{{KEY}}") -ne 0 ]]' ,
1073- '[[ $(printf "%s" "{{KEY}}"; echo 1) -le 0 ]]' ,
1074- '[[ $(cat <<EOF\n{{KEY}}\nEOF\n) -gt 0 ]]' ,
1075- 'let "x={{KEY}}"' ,
1076- 'let x={{KEY}}+1' ,
1077- 'declare -i x="{{KEY}}"' ,
1078- 'typeset -i x={{KEY}}' ,
1079- 'f() { local -i x="{{KEY}}"; }' ,
1080- 'declare -ai values=("{{KEY}}")' ,
1081- 'declare -x -i x="{{KEY}}"' ,
1082- 'let x="$(printf "%s" "{{KEY}}")"' ,
1083- 'declare -i x="$(printf "%s" "{{KEY}}")"' ,
1084- '>/dev/null let x="{{KEY}}"' ,
1085- '2>/dev/null let x="{{KEY}}"' ,
1086- 'builtin let x="{{KEY}}"' ,
1087- 'command let x="{{KEY}}"' ,
1088- 'declare "-i" n="{{KEY}}"' ,
1089- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1090- '[[ ${missing:-{{KEY}}} -eq 0 ]]' ,
1091- 'declare -i n; echo "$(declare +i n)"; n="{{KEY}}"' ,
1092- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1093- 'printf "%s" "${missing:-{text}"; let x="{{KEY}}"' ,
1094- 'declare -i n; n="$(printf "%s" "{{KEY}}")"' ,
1095- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1096- 'declare -i n; n="${missing:-{{KEY}}}"' ,
1097- 'declare -i n; declare n="{{KEY}}"' ,
1098- '> /dev/null let x="{{KEY}}"' ,
1099- 'let x="$(cat <<EOF\n{{KEY}}\nEOF\n)"' ,
1100- 'unset "a[{{KEY}}]"' ,
1101- 'unset a[{{KEY}}]' ,
1102- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1103- 'declare -A m=([key]=abc); printf "%s\\n" "${m[key]:{{KEY}}}"' ,
1104- 'echo $(( 1 < {{KEY}} ))' ,
1105- 'let x=1 >/dev/null {{KEY}}' ,
1106- 'a=([{{KEY}}]=text)' ,
1107- "unset 'a[{{KEY}}]'" ,
1108- 'le\\\nt "x={{KEY}}"' ,
1109- 'local -A a; a[{{KEY}}]=1' ,
1110- 'declare -A m; unset m; m[{{KEY}}]=1' ,
1111- 'declare -A m; unset m; declare -a m; m[{{KEY}}]=1' ,
1112- '(declare -A m); m[{{KEY}}]=1' ,
1113- 'a[{{KEY}}]=1' ,
1114- 'a[{{KEY}}]+=1' ,
1115- 'declare -i n; n="{{KEY}}"' ,
1116- 'declare -i n\nn="{{KEY}}"' ,
1117- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1118- 'a=(1 2); echo "${a[{{KEY}}]}"' ,
1119- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1120- 'a=(1 2); echo "${a[0]:{{KEY}}}"' ,
1121- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1122- 'a=(1 2); echo "${a[0]:0:{{KEY}}}"' ,
1123- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1124- 's=abc; echo "${s:{{KEY}}}"' ,
1125- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1126- 's=abc; echo "${s:0:{{KEY}}}"' ,
1127- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1128- 's=abc; echo "${s: -1:{{KEY}}}"' ,
1129- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1130- 'set -- a b; echo "${@:{{KEY}}}"' ,
1131- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1132- 's=abc; printf "%s\\n" "${missing:-"${s:{{KEY}}}"}"' ,
1133- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1134- 'cat <<PAYLOAD\n${a[{{KEY}}]}\nPAYLOAD' ,
1135- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1136- 'cat <<PAYLOAD\n${s:{{KEY}}}\nPAYLOAD' ,
11371056 ] ) ( 'rejects shell placeholders whose values enter arithmetic: %s' , async ( code ) => {
11381057 await expect (
11391058 compileCodePlaceholders ( {
@@ -1145,7 +1064,7 @@ describe('code placeholder compiler', () => {
11451064 } )
11461065
11471066 it ( 'preserves shell literal arithmetic text and leaves completed arithmetic frames' , async ( ) => {
1148- const value = 'values[1 ]'
1067+ const value = 'values[$(printf injected >&2) ]'
11491068 const compiled = await compileCodePlaceholders ( {
11501069 code : [
11511070 'printf "%s\\n" "{{KEY}}"' ,
@@ -1168,157 +1087,6 @@ describe('code placeholder compiler', () => {
11681087 )
11691088 } )
11701089
1171- it . each ( [
1172- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1173- 'printf "%s\\n" "${missing:-https://{{KEY}}}"' ,
1174- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1175- 'printf "%s\\n" "${missing:-items[{{KEY}}]}"' ,
1176- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1177- 's=abc; printf "%s\\n" "${s#[{{KEY}}]}"' ,
1178- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1179- 's=abc; printf "%s\\n" "${s/[{{KEY}}]/x}"' ,
1180- 'f() { local -i n; }; n="{{KEY}}"' ,
1181- 'PREFIX="$(printf "%s" "{{KEY}}")" let total=2' ,
1182- 'printf "%s" a[{{KEY}}]=1' ,
1183- 'declare -i n; printf "%s" n={{KEY}}' ,
1184- 'declare -i n; declare +i n; n="{{KEY}}"' ,
1185- 'declare -i n; n=1 printf "%s" "{{KEY}}"' ,
1186- 'declare -i n; n=5 text="{{KEY}}"' ,
1187- 'declare -i +i n="{{KEY}}"' ,
1188- 'declare -i n; echo "$(declare +i n; n=\'{{KEY}}\'; printf "%s" "$n")"' ,
1189- '$(let x=1 <<EOF\n{{KEY}}\nEOF\n)' ,
1190- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1191- 'declare -A m; m[{{KEY}}]=x; printf "%s\\n" "${m[{{KEY}}]}"' ,
1192- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1193- 'declare -A m; printf "%s\\n" "${m[{{KEY}}]}"' ,
1194- 'declare -iA m; m[{{KEY}}]=1' ,
1195- 'read -p "items[{{KEY}}]" name' ,
1196- 'a=([{{KEY}}])' ,
1197- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1198- 'printf "%s\\n" "${missing:- # {{KEY}}}"' ,
1199- 'declare -A m; declare -A m; m[{{KEY}}]=1' ,
1200- 'let x=1 >"{{KEY}}"' ,
1201- 'let x=1 > {{KEY}}' ,
1202- 'declare -i n=1 >"{{KEY}}"' ,
1203- '[[ "{{KEY}}" == "-eq" ]]' ,
1204- 'declare -i n; n=1 {{KEY}} hello' ,
1205- 'declare -A m=([{{KEY}}]=x)' ,
1206- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1207- 'declare -A m=([hello]=world); cat <<PAYLOAD\n${m[{{KEY}}]}\nPAYLOAD' ,
1208- ] ) ( 'compiles parameter and prefix text that is not an arithmetic operand: %s' , async ( code ) => {
1209- // These forms use the value as pattern, default, or a shielded prefix — never arithmetic — so a
1210- // conservative scanner must not reject them. Each would fail compilation if wrongly rejected.
1211- await expect (
1212- compileCodePlaceholders ( {
1213- code,
1214- language : CodeLanguage . Shell ,
1215- environmentVariables : { KEY : 'values[1]' } ,
1216- } )
1217- ) . resolves . toBeDefined ( )
1218- } )
1219-
1220- it ( 'keeps literal single quotes inside a double-quoted default expansion' , async ( ) => {
1221- const compiled = await compileCodePlaceholders ( {
1222- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1223- code : 'printf "%s\\n" "${missing:-\'{{KEY}}\'}"' ,
1224- language : CodeLanguage . Shell ,
1225- environmentVariables : { KEY : 'hello world' } ,
1226- } )
1227- expect ( executeShell ( compiled . code , compiled . bindings ) ) . toBe ( "'hello world'\n" )
1228- } )
1229-
1230- it ( 'compiles shell placeholders beside arithmetic that never evaluates them' , async ( ) => {
1231- const value = 'values[1]'
1232- const compiled = await compileCodePlaceholders ( {
1233- code : [
1234- '[ "{{KEY}}" -eq 0 ] 2>/dev/null || printf "%s\\n" not-zero' ,
1235- '[[ "{{KEY}}" == values* && 1 -eq 1 ]] && printf "%s\\n" matched' ,
1236- 'let total=1+1; printf "%s\\n" "{{KEY}}"' ,
1237- 'f() { local copy="{{KEY}}"; printf "%s\\n" "$copy"; }; f' ,
1238- 'declare copy="{{KEY}}"; printf "%s\\n" "$copy"' ,
1239- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1240- 'printf "%s\\n" "${missing:-{{KEY}}}"' ,
1241- // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1242- 'a=(x y); printf "%s\\n" "${a[1]}{{KEY}}"' ,
1243- 'printf "%s\\n" "let {{KEY}}"' ,
1244- 'printf "%s\\n" "items[{{KEY}}]"' ,
1245- 'PREFIX="{{KEY}}" let total=2' ,
1246- '# declare -i n' ,
1247- 'n="{{KEY}}"; printf "%s\\n" "$n"' ,
1248- 'let total=1 <<EOF' ,
1249- '{{KEY}}' ,
1250- 'EOF' ,
1251- 'printf "%s\\n" "$total"' ,
1252- 'cat <<PAYLOAD' ,
1253- '{{KEY}} $(( 1 + 1 ))' ,
1254- 'PAYLOAD' ,
1255- ] . join ( '\n' ) ,
1256- language : CodeLanguage . Shell ,
1257- environmentVariables : { KEY : value } ,
1258- } )
1259-
1260- expect ( executeShell ( `{\n${ compiled . code } \n} 2>&1` , compiled . bindings ) ) . toBe (
1261- `not-zero\nmatched\n${ value } \n${ value } \n${ value } \n${ value } \ny${ value } \n` +
1262- `let ${ value } \nitems[${ value } ]\n${ value } \n1\n${ value } 2\n`
1263- )
1264- } )
1265-
1266- it . each ( [
1267- [ 'an expansion in a subscript' , 'a[$(cmd)]' , 'an array subscript with an expansion' ] ,
1268- [
1269- 'a quoted bracket ending the walk early' ,
1270- 'a["]$(cmd)"]' ,
1271- 'an array subscript with an expansion' ,
1272- ] ,
1273- [ 'a backtick in a subscript' , 'a[`cmd`]' , 'an array subscript with an expansion' ] ,
1274- [ 'a process substitution in a subscript' , 'a[<(cmd)]' , 'an array subscript with an expansion' ] ,
1275- [ 'an escaped expansion in a subscript' , 'a[\\$x]' , 'an array subscript with an expansion' ] ,
1276- [ 'an unclosed subscript' , 'a[' , 'an unbalanced array subscript' ] ,
1277- [ 'a stray closing bracket' , '$(cmd)]' , 'an unbalanced array subscript' ] ,
1278- [ 'an open quote in a subscript' , 'a["]' , 'an unbalanced array subscript' ] ,
1279- ] ) ( 'refuses a shell value with %s wherever it lands' , async ( _case , value , reason ) => {
1280- for ( const code of [ 'printf "%s\\n" "{{KEY}}"' , "cat <<'EOF'\n{{KEY}}\nEOF" ] ) {
1281- await expect (
1282- compileCodePlaceholders ( {
1283- code,
1284- language : CodeLanguage . Shell ,
1285- environmentVariables : { KEY : value } ,
1286- } )
1287- ) . rejects . toThrow ( reason )
1288- }
1289- } )
1290-
1291- it . each ( [ 'values[1]' , '["a", "b"]' , '{"items": [1, 2]}' , '$HOME and $(date)' , "it's [done]" ] ) (
1292- 'compiles a shell value whose subscripts hold no expansion: %s' ,
1293- async ( value ) => {
1294- const compiled = await compileCodePlaceholders ( {
1295- code : 'printf "%s\\n" "{{KEY}}"' ,
1296- language : CodeLanguage . Shell ,
1297- environmentVariables : { KEY : value } ,
1298- } )
1299- expect ( executeShell ( compiled . code , compiled . bindings ) ) . toBe ( `${ value } \n` )
1300- }
1301- )
1302-
1303- it ( 'refuses arithmetic positions whose values are safe alone' , async ( ) => {
1304- // Code can supply the brackets, or split a subscript across adjacent placeholders, so the value
1305- // guard cannot judge these alone; the position scan refuses them.
1306- await expect (
1307- compileCodePlaceholders ( {
1308- code : 'total=$(( a[{{KEY}}] ))' ,
1309- language : CodeLanguage . Shell ,
1310- environmentVariables : { KEY : '$(cmd)' } ,
1311- } )
1312- ) . rejects . toThrow ( 'is not supported in shell arithmetic' )
1313- await expect (
1314- compileCodePlaceholders ( {
1315- code : 'total=$(( {{LEFT}}{{RIGHT}} ))' ,
1316- language : CodeLanguage . Shell ,
1317- environmentVariables : { LEFT : 'a' , RIGHT : '1' } ,
1318- } )
1319- ) . rejects . toThrow ( 'is not supported in shell arithmetic' )
1320- } )
1321-
13221090 it ( 'discovers shell arithmetic placeholders without compiling missing values' , async ( ) => {
13231091 const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
13241092 await expect ( analyzeCodePlaceholders ( code , CodeLanguage . Shell ) ) . resolves . toEqual ( [
0 commit comments