Skip to content

Commit 114091f

Browse files
committed
fix: properly handle multiline strings in command converter
- Added detection for multiline strings to avoid incorrect conversion - Multiline strings now have newlines escaped as \n instead of being joined with semicolons - Line continuations (backslash) are properly distinguished from multiline strings - Added comprehensive test cases for multiline string handling - Fixes issue where multiline strings were incorrectly converted with semicolons
1 parent 4b5ede5 commit 114091f

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

src/utils/__tests__/multilineCommandConverter.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,54 @@ echo "three"`
267267
expect(result.command).toContain('echo "Line 99"')
268268
expect(result.command.split(";").length).toBeGreaterThan(50)
269269
})
270+
271+
it("should handle multiline strings correctly", () => {
272+
const input = `echo "This is a
273+
multiline
274+
string"`
275+
const result = convertMultilineToSingleLine(input)
276+
expect(result.success).toBe(true)
277+
expect(result.command).toBe('echo "This is a\\nmultiline\\nstring"')
278+
})
279+
280+
it("should handle multiline strings with single quotes", () => {
281+
const input = `echo 'First line
282+
Second line
283+
Third line'`
284+
const result = convertMultilineToSingleLine(input)
285+
expect(result.success).toBe(true)
286+
expect(result.command).toBe("echo 'First line\\nSecond line\\nThird line'")
287+
})
288+
289+
it("should handle mixed commands with multiline strings", () => {
290+
const input = `echo "Start"
291+
MESSAGE="This is
292+
a multiline
293+
message"
294+
echo "$MESSAGE"`
295+
const result = convertMultilineToSingleLine(input)
296+
expect(result.success).toBe(true)
297+
expect(result.command).toBe(
298+
'echo "Start" ; MESSAGE="This is\\na multiline\\nmessage" ; echo "$MESSAGE"',
299+
)
300+
})
301+
302+
it("should handle escaped quotes in strings", () => {
303+
const input = `echo "Line with \\"escaped\\" quotes
304+
and a new line"`
305+
const result = convertMultilineToSingleLine(input)
306+
expect(result.success).toBe(true)
307+
expect(result.command).toBe('echo "Line with \\"escaped\\" quotes\\nand a new line"')
308+
})
309+
310+
it("should handle commands after multiline strings", () => {
311+
const input = `TEXT="Line 1
312+
Line 2"
313+
echo "Done"`
314+
const result = convertMultilineToSingleLine(input)
315+
expect(result.success).toBe(true)
316+
expect(result.command).toBe('TEXT="Line 1\\nLine 2" ; echo "Done"')
317+
})
270318
})
271319

272320
describe("Real-world examples", () => {

src/utils/multilineCommandConverter.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,142 @@ function hasHereDocument(command: string): boolean {
2020
return hereDocPattern.test(command)
2121
}
2222

23+
/**
24+
* Detects if a command contains a multiline string literal
25+
*/
26+
function hasMultilineString(command: string): boolean {
27+
// First check if the command has line continuations - these are NOT multiline strings
28+
// Line continuations end with backslash
29+
if (/\\\s*\n/.test(command)) {
30+
return false
31+
}
32+
33+
// Check for multiline strings in quotes that span multiple lines
34+
// This is a simplified check - looks for quotes with newlines between them
35+
const lines = command.split("\n")
36+
let inString = false
37+
let stringDelimiter = ""
38+
let escapeNext = false
39+
40+
for (const line of lines) {
41+
for (let i = 0; i < line.length; i++) {
42+
const char = line[i]
43+
44+
if (escapeNext) {
45+
escapeNext = false
46+
continue
47+
}
48+
49+
if (char === "\\") {
50+
escapeNext = true
51+
continue
52+
}
53+
54+
if ((char === '"' || char === "'") && !inString) {
55+
inString = true
56+
stringDelimiter = char
57+
} else if (char === stringDelimiter && inString) {
58+
inString = false
59+
stringDelimiter = ""
60+
}
61+
}
62+
// If we're still in a string after processing a line, it's multiline
63+
if (inString && lines.indexOf(line) < lines.length - 1) {
64+
return true
65+
}
66+
}
67+
68+
return false
69+
}
70+
71+
/**
72+
* Converts multiline strings to single-line with escaped newlines
73+
*/
74+
function convertMultilineStrings(command: string): string {
75+
// Handle multiline strings by replacing newlines with \n
76+
const lines = command.split("\n")
77+
const result: string[] = []
78+
let inString = false
79+
let stringDelimiter = ""
80+
let currentString = ""
81+
let beforeString = "" // Track text before the string starts
82+
let escapeNext = false
83+
84+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
85+
const line = lines[lineIndex]
86+
let processedLine = ""
87+
88+
for (let i = 0; i < line.length; i++) {
89+
const char = line[i]
90+
91+
if (escapeNext) {
92+
// We're escaping this character
93+
if (inString) {
94+
currentString += "\\" + char
95+
} else {
96+
processedLine += "\\" + char
97+
}
98+
escapeNext = false
99+
continue
100+
}
101+
102+
if (char === "\\") {
103+
// This might be an escape character
104+
const nextChar = i < line.length - 1 ? line[i + 1] : ""
105+
if (nextChar === '"' || nextChar === "'") {
106+
// It's escaping a quote
107+
escapeNext = true
108+
continue
109+
} else {
110+
// It's just a backslash
111+
if (inString) {
112+
currentString += char
113+
} else {
114+
processedLine += char
115+
}
116+
}
117+
} else if ((char === '"' || char === "'") && !inString) {
118+
// Starting a string
119+
inString = true
120+
stringDelimiter = char
121+
beforeString = processedLine // Save text before string
122+
currentString = char
123+
processedLine = "" // Clear processed line as we're now in a string
124+
} else if (char === stringDelimiter && inString) {
125+
// Ending a string
126+
currentString += char
127+
processedLine = beforeString + currentString + processedLine
128+
inString = false
129+
stringDelimiter = ""
130+
currentString = ""
131+
beforeString = ""
132+
} else if (inString) {
133+
// Inside a string
134+
currentString += char
135+
} else {
136+
// Outside a string
137+
processedLine += char
138+
}
139+
}
140+
141+
if (inString && lineIndex < lines.length - 1) {
142+
// We're in a multiline string, add \n for the newline
143+
currentString += "\\n"
144+
} else if (!inString && processedLine) {
145+
// Not in a string, add the processed line
146+
result.push(processedLine)
147+
}
148+
}
149+
150+
// If we're still in a string at the end, complete it
151+
if (inString && currentString) {
152+
// Close the unclosed string and add it with the text before it
153+
result.push(beforeString + currentString)
154+
}
155+
156+
return result.join("\n")
157+
}
158+
23159
/**
24160
* Main function to convert multiline commands to single line
25161
* Uses a simple approach: join lines with semicolons for most cases
@@ -39,6 +175,24 @@ export function convertMultilineToSingleLine(command: string): ConversionResult
39175
}
40176
}
41177

178+
// Check if command contains multiline strings
179+
if (hasMultilineString(command)) {
180+
// Convert multiline strings to single-line with \n
181+
try {
182+
const converted = convertMultilineStrings(command)
183+
// After converting strings, check if there are still multiple lines
184+
if (!converted.includes("\n")) {
185+
return { success: true, command: converted }
186+
}
187+
// If there are still multiple lines after string conversion,
188+
// continue with normal processing
189+
command = converted
190+
} catch (error) {
191+
// If string conversion fails, continue with normal processing
192+
console.log(`[multilineCommandConverter] String conversion failed: ${error.message}`)
193+
}
194+
}
195+
42196
try {
43197
// Simple approach: handle common patterns
44198
let result = command

0 commit comments

Comments
 (0)