diff --git a/src/utils/__tests__/multilineCommandConverter.spec.ts b/src/utils/__tests__/multilineCommandConverter.spec.ts index 7ea0d7b23a..e288fc91f7 100644 --- a/src/utils/__tests__/multilineCommandConverter.spec.ts +++ b/src/utils/__tests__/multilineCommandConverter.spec.ts @@ -267,6 +267,54 @@ echo "three"` expect(result.command).toContain('echo "Line 99"') expect(result.command.split(";").length).toBeGreaterThan(50) }) + + it("should handle multiline strings correctly", () => { + const input = `echo "This is a +multiline +string"` + const result = convertMultilineToSingleLine(input) + expect(result.success).toBe(true) + expect(result.command).toBe('echo "This is a\\nmultiline\\nstring"') + }) + + it("should handle multiline strings with single quotes", () => { + const input = `echo 'First line +Second line +Third line'` + const result = convertMultilineToSingleLine(input) + expect(result.success).toBe(true) + expect(result.command).toBe("echo 'First line\\nSecond line\\nThird line'") + }) + + it("should handle mixed commands with multiline strings", () => { + const input = `echo "Start" +MESSAGE="This is +a multiline +message" +echo "$MESSAGE"` + const result = convertMultilineToSingleLine(input) + expect(result.success).toBe(true) + expect(result.command).toBe( + 'echo "Start" ; MESSAGE="This is\\na multiline\\nmessage" ; echo "$MESSAGE"', + ) + }) + + it("should handle escaped quotes in strings", () => { + const input = `echo "Line with \\"escaped\\" quotes +and a new line"` + const result = convertMultilineToSingleLine(input) + expect(result.success).toBe(true) + expect(result.command).toBe('echo "Line with \\"escaped\\" quotes\\nand a new line"') + }) + + it("should handle commands after multiline strings", () => { + const input = `TEXT="Line 1 +Line 2" +echo "Done"` + const result = convertMultilineToSingleLine(input) + expect(result.success).toBe(true) + expect(result.command).toBe('TEXT="Line 1\\nLine 2" ; echo "Done"') + }) }) describe("Real-world examples", () => { diff --git a/src/utils/multilineCommandConverter.ts b/src/utils/multilineCommandConverter.ts index 0fb7b5b3f0..3268cbfa26 100644 --- a/src/utils/multilineCommandConverter.ts +++ b/src/utils/multilineCommandConverter.ts @@ -20,6 +20,142 @@ function hasHereDocument(command: string): boolean { return hereDocPattern.test(command) } +/** + * Detects if a command contains a multiline string literal + */ +function hasMultilineString(command: string): boolean { + // First check if the command has line continuations - these are NOT multiline strings + // Line continuations end with backslash + if (/\\\s*\n/.test(command)) { + return false + } + + // Check for multiline strings in quotes that span multiple lines + // This is a simplified check - looks for quotes with newlines between them + const lines = command.split("\n") + let inString = false + let stringDelimiter = "" + let escapeNext = false + + for (const line of lines) { + for (let i = 0; i < line.length; i++) { + const char = line[i] + + if (escapeNext) { + escapeNext = false + continue + } + + if (char === "\\") { + escapeNext = true + continue + } + + if ((char === '"' || char === "'") && !inString) { + inString = true + stringDelimiter = char + } else if (char === stringDelimiter && inString) { + inString = false + stringDelimiter = "" + } + } + // If we're still in a string after processing a line, it's multiline + if (inString && lines.indexOf(line) < lines.length - 1) { + return true + } + } + + return false +} + +/** + * Converts multiline strings to single-line with escaped newlines + */ +function convertMultilineStrings(command: string): string { + // Handle multiline strings by replacing newlines with \n + const lines = command.split("\n") + const result: string[] = [] + let inString = false + let stringDelimiter = "" + let currentString = "" + let beforeString = "" // Track text before the string starts + let escapeNext = false + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex] + let processedLine = "" + + for (let i = 0; i < line.length; i++) { + const char = line[i] + + if (escapeNext) { + // We're escaping this character + if (inString) { + currentString += "\\" + char + } else { + processedLine += "\\" + char + } + escapeNext = false + continue + } + + if (char === "\\") { + // This might be an escape character + const nextChar = i < line.length - 1 ? line[i + 1] : "" + if (nextChar === '"' || nextChar === "'") { + // It's escaping a quote + escapeNext = true + continue + } else { + // It's just a backslash + if (inString) { + currentString += char + } else { + processedLine += char + } + } + } else if ((char === '"' || char === "'") && !inString) { + // Starting a string + inString = true + stringDelimiter = char + beforeString = processedLine // Save text before string + currentString = char + processedLine = "" // Clear processed line as we're now in a string + } else if (char === stringDelimiter && inString) { + // Ending a string + currentString += char + processedLine = beforeString + currentString + processedLine + inString = false + stringDelimiter = "" + currentString = "" + beforeString = "" + } else if (inString) { + // Inside a string + currentString += char + } else { + // Outside a string + processedLine += char + } + } + + if (inString && lineIndex < lines.length - 1) { + // We're in a multiline string, add \n for the newline + currentString += "\\n" + } else if (!inString && processedLine) { + // Not in a string, add the processed line + result.push(processedLine) + } + } + + // If we're still in a string at the end, complete it + if (inString && currentString) { + // Close the unclosed string and add it with the text before it + result.push(beforeString + currentString) + } + + return result.join("\n") +} + /** * Main function to convert multiline commands to single line * Uses a simple approach: join lines with semicolons for most cases @@ -39,6 +175,24 @@ export function convertMultilineToSingleLine(command: string): ConversionResult } } + // Check if command contains multiline strings + if (hasMultilineString(command)) { + // Convert multiline strings to single-line with \n + try { + const converted = convertMultilineStrings(command) + // After converting strings, check if there are still multiple lines + if (!converted.includes("\n")) { + return { success: true, command: converted } + } + // If there are still multiple lines after string conversion, + // continue with normal processing + command = converted + } catch (error) { + // If string conversion fails, continue with normal processing + console.log(`[multilineCommandConverter] String conversion failed: ${error.message}`) + } + } + try { // Simple approach: handle common patterns let result = command