Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/sim/lib/execution/code-placeholders/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,64 @@ describe('code placeholder compiler', () => {
}
})

it.each([
'total=$(( {{KEY}} * 2 ))',
'printf "%s" "$(( {{KEY}} * 2 ))"',
'total=$(( "{{KEY}}" * 2 ))',
'total=$[ {{KEY}} * 2 ]',
'printf "%s" "$[ {{KEY}} * 2 ]"',
'total=$[ values[0] + {{KEY}} ]',
'(( total = {{KEY}} * 2 ))',
'for (( i = {{KEY}}; i < 2; i++ )); do :; done',
'total=$(( $(printf "%s" "{{KEY}}") * 2 ))',
'total=$(( `printf "%s" "{{KEY}}"` * 2 ))',
'total=$(( $(( 1 + 1 )) + {{KEY}} ))',
'cat <<PAYLOAD\n$(( {{KEY}} * 2 ))\nPAYLOAD',
'cat <<PAYLOAD\n$[ {{KEY}} * 2 ]\nPAYLOAD',
])('rejects shell placeholders whose values enter arithmetic: %s', async (code) => {
await expect(
compileCodePlaceholders({
code,
language: CodeLanguage.Shell,
environmentVariables: { KEY: 'values[$(printf injected >&2)]' },
})
).rejects.toThrow('is not supported in shell arithmetic')
})

it('preserves shell literal arithmetic text and leaves completed arithmetic frames', async () => {
const value = 'values[$(printf injected >&2)]'
const compiled = await compileCodePlaceholders({
code: [
'printf "%s\\n" "{{KEY}}"',
"printf '%s\\n' '$(( {{KEY}} ))'",
"printf '%s\\n' '$[ {{KEY}} ]'",
'printf "%s\\n" "$(printf %s "{{KEY}}")"',
'printf "%s\\n" "$(( 1 + 1 )){{KEY}}"',
'printf "%s\\n" "$[ values[0] + 2 ]{{KEY}}"',
'(( total = 2 )); printf "%s\\n" "{{KEY}}"',
'cat <<PAYLOAD',
'(( {{KEY}} ))',
'PAYLOAD',
].join('\n'),
language: CodeLanguage.Shell,
environmentVariables: { KEY: value },
})

expect(executeShell(compiled.code, compiled.bindings)).toBe(
`${value}\n$(( ${value} ))\n$[ ${value} ]\n${value}\n2${value}\n2${value}\n${value}\n(( ${value} ))\n`
)
})

it('discovers shell arithmetic placeholders without compiling missing values', async () => {
const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
await expect(analyzeCodePlaceholders(code, CodeLanguage.Shell)).resolves.toEqual([
'MISSING',
'KEY',
])
const compiled = await compileCodePlaceholders({ code, language: CodeLanguage.Shell })
expect(compiled.code).toBe(code)
})

it('renders quoted shell heredocs nested in double-quoted command substitutions', async () => {
const compiled = await compileCodePlaceholders({
code: [
Expand Down
56 changes: 47 additions & 9 deletions apps/sim/lib/execution/code-placeholders/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ interface ShellScanFrame {
kind: 'root' | 'command' | 'arithmetic' | 'backtick'
quote: ShellQuote
parenthesisDepth: number
bracketDepth?: number
literalRoot: boolean
}

interface ShellOccurrenceContext {
quote: ShellQuote
/** Includes nested command substitutions whose output can become an arithmetic operand. */
arithmetic?: boolean
unsupported?: 'escaped sequence'
}

Expand Down Expand Up @@ -445,10 +448,11 @@ function isShellAssignmentName(code: string, occurrence: CodePlaceholderOccurren
function getUnsupportedShellPosition(
code: string,
occurrence: CodePlaceholderOccurrence,
quote: ShellQuote
context: ShellOccurrenceContext
): string | undefined {
if (context.arithmetic) return 'in shell arithmetic'
if (code[occurrence.start - 1] === '$') return 'immediately after "$"'
if (quote !== 'none') return undefined
if (context.quote !== 'none') return undefined

const lineStart = Math.max(
code.lastIndexOf('\n', occurrence.start - 1),
Expand Down Expand Up @@ -488,6 +492,7 @@ function collectShellOccurrenceContexts(
{ kind: 'root', quote: 'none', parenthesisDepth: 0, literalRoot },
]
let skippedRangeIndex = 0
let arithmeticDepth = 0

for (let index = start; index < end; ) {
const frame = frames.at(-1)
Expand All @@ -507,7 +512,7 @@ function collectShellOccurrenceContexts(

const occurrence = occurrenceByStart.get(index)
if (occurrence) {
contexts.set(occurrence, { quote: frame.quote })
contexts.set(occurrence, { quote: frame.quote, arithmetic: arithmeticDepth > 0 })
index = occurrence.end
continue
}
Expand All @@ -533,6 +538,24 @@ function collectShellOccurrenceContexts(
}
continue
}
const arithmeticExpansion =
Comment thread
icecrasher321 marked this conversation as resolved.
character === '$' &&
((code[index + 1] === '(' && code[index + 2] === '(') || code[index + 1] === '[')
const arithmeticCommand =
frame.quote === 'none' && !frame.literalRoot && shellArithmeticCommandStarts(code, index)
if (arithmeticExpansion || arithmeticCommand) {
Comment thread
icecrasher321 marked this conversation as resolved.
const brackets = arithmeticExpansion && code[index + 1] === '['
frames.push({
kind: 'arithmetic',
quote: 'none',
parenthesisDepth: brackets ? 0 : 2,
...(brackets ? { bracketDepth: 1 } : {}),
literalRoot: false,
})
arithmeticDepth += 1
index += arithmeticExpansion && !brackets ? 3 : 2
continue
}
if (frame.quote === 'double') {
if (character === '\\') {
const escaped = occurrenceByStart.get(index + 1)
Expand Down Expand Up @@ -572,7 +595,7 @@ function collectShellOccurrenceContexts(
index += 1
continue
}
if (!frame.literalRoot && shellCommentStarts(code, index)) {
if (frame.kind !== 'arithmetic' && !frame.literalRoot && shellCommentStarts(code, index)) {
const newline = code.indexOf('\n', index)
index = newline === -1 || newline >= end ? end : newline + 1
continue
Expand Down Expand Up @@ -622,14 +645,29 @@ function collectShellOccurrenceContexts(
index += 1
continue
}
if (frame.kind === 'command' && character === '(') {
if (frame.kind === 'arithmetic' && frame.bracketDepth !== undefined) {
if (character === '[') frame.bracketDepth += 1
if (character === ']') {
frame.bracketDepth -= 1
if (frame.bracketDepth === 0) {
frames.pop()
arithmeticDepth -= 1
}
}
index += 1
continue
}
if ((frame.kind === 'command' || frame.kind === 'arithmetic') && character === '(') {
frame.parenthesisDepth += 1
index += 1
continue
}
if (frame.kind === 'command' && character === ')') {
if ((frame.kind === 'command' || frame.kind === 'arithmetic') && character === ')') {
frame.parenthesisDepth -= 1
if (frame.parenthesisDepth === 0) frames.pop()
if (frame.parenthesisDepth === 0) {
frames.pop()
if (frame.kind === 'arithmetic') arithmeticDepth -= 1
}
index += 1
continue
}
Expand Down Expand Up @@ -849,7 +887,7 @@ export async function compileShellPlaceholders(
const unsupportedPosition = getUnsupportedShellPosition(
input.code,
occurrence,
occurrenceContext.quote
occurrenceContext
)
if (unsupportedPosition) {
if (context.hasValue(occurrence.name)) {
Expand Down Expand Up @@ -904,7 +942,7 @@ export async function compileShellPlaceholders(
const unsupportedPosition = getUnsupportedShellPosition(
input.code,
occurrence,
occurrenceContext.quote
occurrenceContext
)
if (unsupportedPosition) {
if (context.hasValue(occurrence.name)) {
Expand Down
Loading