Skip to content

Commit 54005dd

Browse files
committed
fix(shell): reject placeholders in numeric conditional operands
1 parent b9b0e8c commit 54005dd

2 files changed

Lines changed: 112 additions & 8 deletions

File tree

‎apps/sim/lib/execution/code-placeholders/compiler.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,13 @@ describe('code placeholder compiler', () => {
10321032
'if values[{{KEY}}]=x; then :; fi',
10331033
'declare -a values[{{KEY}}]=x',
10341034
'printf %s done # end of command\nvalues[{{KEY}}]=x',
1035+
'[[ 1 -eq {{KEY}} ]]',
1036+
'[[ "{{KEY}}" -ne 1 ]]',
1037+
'[[ 1 -lt "{{KEY}}" ]]',
1038+
'[[ {{KEY}} -le 1 ]]',
1039+
'[[ 1 -gt {{KEY}} ]]',
1040+
'[[ {{KEY}} -ge 1 ]]',
1041+
'[[ "$(printf %s "{{KEY}}")" -eq 1 ]]',
10351042
'values[ 1 + {{KEY}} ]=x',
10361043
'values[$(printf %s "{{KEY}}")]=x',
10371044
'values=([{{KEY}}]=x)',
@@ -1111,6 +1118,18 @@ describe('code placeholder compiler', () => {
11111118
)
11121119
})
11131120

1121+
it('preserves string operands beside numeric shell comparisons', async () => {
1122+
const compiled = await compileCodePlaceholders({
1123+
code: [
1124+
'[[ "{{KEY}}" == word && 1 -eq 1 ]] && printf "%s\\n" "{{KEY}}"',
1125+
'[[ 2 -gt 1 && ( "{{KEY}}" == word || "{{KEY}}" == "-eq" ) ]] && printf "%s\\n" "{{KEY}}"',
1126+
].join('\n'),
1127+
language: CodeLanguage.Shell,
1128+
environmentVariables: { KEY: 'word' },
1129+
})
1130+
expect(executeShell(compiled.code, compiled.bindings)).toBe('word\nword\n')
1131+
})
1132+
11141133
it.each([
11151134
['unquoted', "cat <<EOF >/dev/null\nToday's report\nEOF"],
11161135
['quoted', "cat <<'EOF' >/dev/null\nToday's report\nEOF"],

‎apps/sim/lib/execution/code-placeholders/shell.ts‎

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,19 @@ interface HeredocDeclaration {
2626
type ShellQuote = 'none' | 'single' | 'double' | 'ansi'
2727

2828
interface ShellScanFrame {
29-
kind: 'root' | 'command' | 'arithmetic' | 'backtick' | 'array'
29+
kind: 'root' | 'command' | 'arithmetic' | 'backtick' | 'array' | 'conditional'
3030
quote: ShellQuote
3131
parenthesisDepth: number
3232
bracketDepth?: number
33-
/** A bare name[index] is arithmetic only when its closing bracket is followed by assignment. */
34-
arrayAssignment?: boolean
33+
/** Some operands become arithmetic only after a following assignment or comparison operator. */
34+
arithmeticEnabled?: boolean
3535
arithmeticParent?: ShellScanFrame
36+
conditional?: {
37+
parent?: ShellScanFrame
38+
previousOperand?: ShellScanFrame
39+
word?: ShellScanFrame
40+
numericOperand: boolean
41+
}
3642
wordStart?: number
3743
commandPosition?: boolean
3844
declarationCommand?: boolean
@@ -142,6 +148,7 @@ function readShellArrayStart(
142148
const assignment =
143149
parent.quote === 'none' &&
144150
!parent.literalRoot &&
151+
parent.kind !== 'conditional' &&
145152
wordStart &&
146153
(parent.kind === 'array' || parent.commandPosition !== false || parent.declarationCommand) &&
147154
(hasName || (character === '[' && parent.kind === 'array'))
@@ -152,7 +159,7 @@ function readShellArrayStart(
152159
quote: 'none',
153160
parenthesisDepth: character === '(' ? 1 : 0,
154161
...(character === '['
155-
? { bracketDepth: 1, ...(!parameter ? { arrayAssignment: false } : {}) }
162+
? { bracketDepth: 1, ...(!parameter ? { arithmeticEnabled: false } : {}) }
156163
: {}),
157164
literalRoot: false,
158165
}
@@ -333,6 +340,52 @@ function trackShellCommandPosition(code: string, index: number, frame: ShellScan
333340
}
334341
}
335342

343+
/** Keep operand contexts pending until the following conditional operator is known. */
344+
function trackShellConditionalOperand(code: string, index: number, frame: ShellScanFrame): void {
345+
const conditional = frame.conditional
346+
if (!conditional || frame.quote !== 'none') return
347+
const character = code[index]
348+
const closing =
349+
frame.wordStart === undefined &&
350+
code.startsWith(']]', index) &&
351+
(index + 2 === code.length || /\s|[;&|()<>]/.test(code[index + 2]))
352+
const separator = /[&|()]/.test(character) || closing
353+
if (!separator && !/\s/.test(character)) {
354+
if (character === '\\' && /^\r?\n/.test(code.slice(index + 1, index + 3))) return
355+
if (frame.wordStart === undefined) {
356+
frame.wordStart = index
357+
conditional.word = {
358+
kind: 'arithmetic',
359+
quote: 'none',
360+
parenthesisDepth: 0,
361+
literalRoot: false,
362+
arithmeticEnabled: conditional.numericOperand,
363+
arithmeticParent: conditional.parent,
364+
}
365+
conditional.numericOperand = false
366+
frame.arithmeticParent = conditional.word
367+
}
368+
return
369+
}
370+
371+
if (frame.wordStart !== undefined) {
372+
const word = code.slice(frame.wordStart, index).replace(/\\\r?\n/g, '')
373+
if (/^-(?:eq|ne|lt|le|gt|ge)$/.test(word)) {
374+
if (conditional.previousOperand) conditional.previousOperand.arithmeticEnabled = true
375+
conditional.numericOperand = true
376+
} else {
377+
conditional.previousOperand = conditional.word
378+
}
379+
frame.wordStart = undefined
380+
conditional.word = undefined
381+
frame.arithmeticParent = conditional.parent
382+
}
383+
if (separator) {
384+
conditional.previousOperand = undefined
385+
conditional.numericOperand = false
386+
}
387+
}
388+
336389
function parseHeredocHeaders(
337390
code: string,
338391
lineStart: number,
@@ -542,7 +595,7 @@ function getUnsupportedShellPosition(
542595
context: ShellOccurrenceContext
543596
): string | undefined {
544597
for (let frame = context.arithmeticFrame; frame; frame = frame.arithmeticParent) {
545-
if (frame.arrayAssignment !== false) return 'in a shell arithmetic expression'
598+
if (frame.arithmeticEnabled !== false) return 'in a shell arithmetic expression'
546599
}
547600
if (code[occurrence.start - 1] === '$') return 'immediately after "$"'
548601
if (context.quote !== 'none') return undefined
@@ -588,6 +641,7 @@ function collectShellOccurrenceContexts(
588641
if (!frame) break
589642

590643
trackShellCommandPosition(code, index, frame)
644+
trackShellConditionalOperand(code, index, frame)
591645
const occurrence = occurrenceByStart.get(index)
592646
if (occurrence) {
593647
contexts.set(occurrence, {
@@ -622,7 +676,10 @@ function collectShellOccurrenceContexts(
622676
const expansion = readShellExpansionStart(
623677
code,
624678
index,
625-
frame.quote === 'none' && !frame.literalRoot && frame.kind !== 'arithmetic'
679+
frame.quote === 'none' &&
680+
!frame.literalRoot &&
681+
frame.kind !== 'arithmetic' &&
682+
frame.kind !== 'conditional'
626683
)
627684
const array = readShellArrayStart(code, index, frame)
628685
if (array) {
@@ -664,6 +721,34 @@ function collectShellOccurrenceContexts(
664721
index += 1
665722
continue
666723
}
724+
if (
725+
frame.kind === 'conditional' &&
726+
frame.wordStart === undefined &&
727+
code.startsWith(']]', index)
728+
) {
729+
frames.pop()
730+
index += 2
731+
continue
732+
}
733+
if (
734+
!frame.literalRoot &&
735+
frame.commandPosition !== false &&
736+
code.startsWith('[[', index) &&
737+
(index === 0 || /\s|[;&|()]/.test(code[index - 1])) &&
738+
(index + 2 === end || /\s|[()]/.test(code[index + 2]))
739+
) {
740+
const conditional: ShellScanFrame = {
741+
kind: 'conditional',
742+
quote: 'none',
743+
parenthesisDepth: 0,
744+
literalRoot: false,
745+
conditional: { numericOperand: false },
746+
}
747+
pushShellFrame(frames, conditional)
748+
if (conditional.conditional) conditional.conditional.parent = conditional.arithmeticParent
749+
index += 2
750+
continue
751+
}
667752
if (!frame.literalRoot && shellCommentStarts(code, index)) {
668753
const newline = code.indexOf('\n', index)
669754
if (newline !== -1 && newline < end) trackShellCommandPosition(code, newline, frame)
@@ -715,8 +800,8 @@ function collectShellOccurrenceContexts(
715800
if (character === ']') {
716801
frame.bracketDepth -= 1
717802
if (frame.bracketDepth === 0) {
718-
if (frame.arrayAssignment !== undefined) {
719-
frame.arrayAssignment = code[index + 1] === '=' || code.startsWith('+=', index + 1)
803+
if (frame.arithmeticEnabled !== undefined) {
804+
frame.arithmeticEnabled = code[index + 1] === '=' || code.startsWith('+=', index + 1)
720805
}
721806
frames.pop()
722807
}

0 commit comments

Comments
 (0)