@@ -1053,6 +1053,78 @@ 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+ 'if [[ 0 -lt {{KEY}} ]]; then echo positive; fi' ,
1062+ '[[ -n x && ( "{{KEY}}" -ge 1 ) ]]' ,
1063+ '[[ $(printf "%s" "{{KEY}}") -ne 0 ]]' ,
1064+ '[[ $(printf "%s" "{{KEY}}"; echo 1) -le 0 ]]' ,
1065+ '[[ $(cat <<EOF\n{{KEY}}\nEOF\n) -gt 0 ]]' ,
1066+ 'let "x={{KEY}}"' ,
1067+ 'let x={{KEY}}+1' ,
1068+ 'declare -i x="{{KEY}}"' ,
1069+ 'typeset -i x={{KEY}}' ,
1070+ 'f() { local -i x="{{KEY}}"; }' ,
1071+ 'declare -ai values=("{{KEY}}")' ,
1072+ 'declare -x -i x="{{KEY}}"' ,
1073+ 'let x="$(printf "%s" "{{KEY}}")"' ,
1074+ 'declare -i x="$(printf "%s" "{{KEY}}")"' ,
1075+ '>/dev/null let x="{{KEY}}"' ,
1076+ '2>/dev/null let x="{{KEY}}"' ,
1077+ 'builtin let x="{{KEY}}"' ,
1078+ 'command let x="{{KEY}}"' ,
1079+ 'declare "-i" n="{{KEY}}"' ,
1080+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1081+ '[[ ${missing:-{{KEY}}} -eq 0 ]]' ,
1082+ 'declare -i n; echo "$(declare +i n)"; n="{{KEY}}"' ,
1083+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1084+ 'printf "%s" "${missing:-{text}"; let x="{{KEY}}"' ,
1085+ 'declare -i n; n="$(printf "%s" "{{KEY}}")"' ,
1086+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1087+ 'declare -i n; n="${missing:-{{KEY}}}"' ,
1088+ 'declare -i n; declare n="{{KEY}}"' ,
1089+ '> /dev/null let x="{{KEY}}"' ,
1090+ 'let x="$(cat <<EOF\n{{KEY}}\nEOF\n)"' ,
1091+ 'unset "a[{{KEY}}]"' ,
1092+ 'unset a[{{KEY}}]' ,
1093+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1094+ 'declare -A m=([key]=abc); printf "%s\\n" "${m[key]:{{KEY}}}"' ,
1095+ 'echo $(( 1 < {{KEY}} ))' ,
1096+ 'let x=1 >/dev/null {{KEY}}' ,
1097+ 'a=([{{KEY}}]=text)' ,
1098+ "unset 'a[{{KEY}}]'" ,
1099+ 'le\\\nt "x={{KEY}}"' ,
1100+ 'local -A a; a[{{KEY}}]=1' ,
1101+ 'declare -A m; unset m; m[{{KEY}}]=1' ,
1102+ 'declare -A m; unset m; declare -a m; m[{{KEY}}]=1' ,
1103+ '(declare -A m); m[{{KEY}}]=1' ,
1104+ 'a[{{KEY}}]=1' ,
1105+ 'a[{{KEY}}]+=1' ,
1106+ 'declare -i n; n="{{KEY}}"' ,
1107+ 'declare -i n\nn="{{KEY}}"' ,
1108+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1109+ 'a=(1 2); echo "${a[{{KEY}}]}"' ,
1110+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1111+ 'a=(1 2); echo "${a[0]:{{KEY}}}"' ,
1112+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1113+ 'a=(1 2); echo "${a[0]:0:{{KEY}}}"' ,
1114+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1115+ 's=abc; echo "${s:{{KEY}}}"' ,
1116+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1117+ 's=abc; echo "${s:0:{{KEY}}}"' ,
1118+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1119+ 's=abc; echo "${s: -1:{{KEY}}}"' ,
1120+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1121+ 'set -- a b; echo "${@:{{KEY}}}"' ,
1122+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1123+ 's=abc; printf "%s\\n" "${missing:-"${s:{{KEY}}}"}"' ,
1124+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1125+ 'cat <<PAYLOAD\n${a[{{KEY}}]}\nPAYLOAD' ,
1126+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1127+ 'cat <<PAYLOAD\n${s:{{KEY}}}\nPAYLOAD' ,
10561128 ] ) ( 'rejects shell placeholders whose values enter arithmetic: %s' , async ( code ) => {
10571129 await expect (
10581130 compileCodePlaceholders ( {
@@ -1087,6 +1159,96 @@ describe('code placeholder compiler', () => {
10871159 )
10881160 } )
10891161
1162+ it . each ( [
1163+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1164+ 'printf "%s\\n" "${missing:-https://{{KEY}}}"' ,
1165+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1166+ 'printf "%s\\n" "${missing:-items[{{KEY}}]}"' ,
1167+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1168+ 's=abc; printf "%s\\n" "${s#[{{KEY}}]}"' ,
1169+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1170+ 's=abc; printf "%s\\n" "${s/[{{KEY}}]/x}"' ,
1171+ 'f() { local -i n; }; n="{{KEY}}"' ,
1172+ 'PREFIX="$(printf "%s" "{{KEY}}")" let total=2' ,
1173+ 'printf "%s" a[{{KEY}}]=1' ,
1174+ 'declare -i n; printf "%s" n={{KEY}}' ,
1175+ 'declare -i n; declare +i n; n="{{KEY}}"' ,
1176+ 'declare -i n; n=1 printf "%s" "{{KEY}}"' ,
1177+ 'declare -i n; n=5 text="{{KEY}}"' ,
1178+ 'declare -i +i n="{{KEY}}"' ,
1179+ 'declare -i n; echo "$(declare +i n; n=\'{{KEY}}\'; printf "%s" "$n")"' ,
1180+ '$(let x=1 <<EOF\n{{KEY}}\nEOF\n)' ,
1181+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1182+ 'declare -A m; m[{{KEY}}]=x; printf "%s\\n" "${m[{{KEY}}]}"' ,
1183+ 'declare -A m; printf "%s\\n" "${m[{{KEY}}]}"' ,
1184+ 'declare -iA m; m[{{KEY}}]=1' ,
1185+ 'read -p "items[{{KEY}}]" name' ,
1186+ 'a=([{{KEY}}])' ,
1187+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1188+ 'printf "%s\\n" "${missing:- # {{KEY}}}"' ,
1189+ 'declare -A m; declare -A m; m[{{KEY}}]=1' ,
1190+ 'let x=1 >"{{KEY}}"' ,
1191+ 'let x=1 > {{KEY}}' ,
1192+ 'declare -i n=1 >"{{KEY}}"' ,
1193+ '[[ "{{KEY}}" == "-eq" ]]' ,
1194+ ] ) ( 'compiles parameter and prefix text that is not an arithmetic operand: %s' , async ( code ) => {
1195+ // These forms use the value as pattern, default, or a shielded prefix — never arithmetic — so a
1196+ // conservative scanner must not reject them. Each would fail compilation if wrongly rejected.
1197+ await expect (
1198+ compileCodePlaceholders ( {
1199+ code,
1200+ language : CodeLanguage . Shell ,
1201+ environmentVariables : { KEY : 'values[$(printf injected >&2)]' } ,
1202+ } )
1203+ ) . resolves . toBeDefined ( )
1204+ } )
1205+
1206+ it ( 'keeps literal single quotes inside a double-quoted default expansion' , async ( ) => {
1207+ const compiled = await compileCodePlaceholders ( {
1208+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1209+ code : 'printf "%s\\n" "${missing:-\'{{KEY}}\'}"' ,
1210+ language : CodeLanguage . Shell ,
1211+ environmentVariables : { KEY : 'hello world' } ,
1212+ } )
1213+ expect ( executeShell ( compiled . code , compiled . bindings ) ) . toBe ( "'hello world'\n" )
1214+ } )
1215+
1216+ it ( 'compiles shell placeholders beside arithmetic that never evaluates them' , async ( ) => {
1217+ const value = 'values[$(printf injected >&2)]'
1218+ const compiled = await compileCodePlaceholders ( {
1219+ code : [
1220+ '[ "{{KEY}}" -eq 0 ] 2>/dev/null || printf "%s\\n" not-zero' ,
1221+ '[[ "{{KEY}}" == values* && 1 -eq 1 ]] && printf "%s\\n" matched' ,
1222+ 'let total=1+1; printf "%s\\n" "{{KEY}}"' ,
1223+ 'f() { local copy="{{KEY}}"; printf "%s\\n" "$copy"; }; f' ,
1224+ 'declare copy="{{KEY}}"; printf "%s\\n" "$copy"' ,
1225+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1226+ 'printf "%s\\n" "${missing:-{{KEY}}}"' ,
1227+ // biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1228+ 'a=(x y); printf "%s\\n" "${a[1]}{{KEY}}"' ,
1229+ 'printf "%s\\n" "let {{KEY}}"' ,
1230+ 'printf "%s\\n" "items[{{KEY}}]"' ,
1231+ 'PREFIX="{{KEY}}" let total=2' ,
1232+ '# declare -i n' ,
1233+ 'n="{{KEY}}"; printf "%s\\n" "$n"' ,
1234+ 'let total=1 <<EOF' ,
1235+ '{{KEY}}' ,
1236+ 'EOF' ,
1237+ 'printf "%s\\n" "$total"' ,
1238+ 'cat <<PAYLOAD' ,
1239+ '{{KEY}} $(( 1 + 1 ))' ,
1240+ 'PAYLOAD' ,
1241+ ] . join ( '\n' ) ,
1242+ language : CodeLanguage . Shell ,
1243+ environmentVariables : { KEY : value } ,
1244+ } )
1245+
1246+ expect ( executeShell ( `{\n${ compiled . code } \n} 2>&1` , compiled . bindings ) ) . toBe (
1247+ `not-zero\nmatched\n${ value } \n${ value } \n${ value } \n${ value } \ny${ value } \n` +
1248+ `let ${ value } \nitems[${ value } ]\n${ value } \n1\n${ value } 2\n`
1249+ )
1250+ } )
1251+
10901252 it ( 'discovers shell arithmetic placeholders without compiling missing values' , async ( ) => {
10911253 const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
10921254 await expect ( analyzeCodePlaceholders ( code , CodeLanguage . Shell ) ) . resolves . toEqual ( [
0 commit comments