From 9e5599c323f12df36672794332df5ae16e6125d5 Mon Sep 17 00:00:00 2001 From: Cesar Sanchez Coraspe Date: Fri, 12 Jun 2026 13:01:46 -0600 Subject: [PATCH] fix(core): handle multi-line escaped quotes in stripShellWrapper (#27467) Co-authored-by: luisfelipe-alt --- packages/core/src/utils/shell-utils.test.ts | 16 +++++++++++--- packages/core/src/utils/shell-utils.ts | 24 +++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/core/src/utils/shell-utils.test.ts b/packages/core/src/utils/shell-utils.test.ts index bb46383709..0e9034a8c7 100644 --- a/packages/core/src/utils/shell-utils.test.ts +++ b/packages/core/src/utils/shell-utils.test.ts @@ -55,9 +55,13 @@ vi.mock('node:child_process', () => ({ })); const mockQuote = vi.hoisted(() => vi.fn()); -vi.mock('shell-quote', () => ({ - quote: mockQuote, -})); +vi.mock('shell-quote', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + quote: mockQuote, + }; +}); const mockDebugLogger = vi.hoisted(() => ({ error: vi.fn(), @@ -388,6 +392,12 @@ describe('stripShellWrapper', () => { it('should not strip anything if no wrapper is present', () => { expect(stripShellWrapper('ls -l')).toEqual('ls -l'); }); + + it('should handle multi-line escaped double quotes correctly', () => { + const multiLine = 'bash -c "hg commit -m \\"title\n\nbody\\""'; + const expected = 'hg commit -m "title\n\nbody"'; + expect(stripShellWrapper(multiLine)).toEqual(expected); + }); }); describe('escapeShellArg', () => { diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index 020eb57484..dc2df2e32d 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -7,7 +7,7 @@ import os from 'node:os'; import fs from 'node:fs'; import path from 'node:path'; -import { quote, type ParseEntry } from 'shell-quote'; +import { quote, parse, type ParseEntry } from 'shell-quote'; import { spawn, spawnSync, @@ -846,10 +846,26 @@ export function stripShellWrapper(command: string): string { if (match) { let newCommand = command.substring(match[0].length).trim(); if ( - (newCommand.startsWith('"') && newCommand.endsWith('"')) || - (newCommand.startsWith("'") && newCommand.endsWith("'")) + newCommand.length >= 2 && + ((newCommand.startsWith('"') && newCommand.endsWith('"')) || + (newCommand.startsWith("'") && newCommand.endsWith("'"))) ) { - newCommand = newCommand.substring(1, newCommand.length - 1); + const isPosixShell = match[0].trim().endsWith('-c'); + if (isPosixShell && newCommand.startsWith('"')) { + try { + const parsed = parse(newCommand, (key) => '$' + key); + const firstEntry = parsed[0]; + if (parsed.length === 1 && typeof firstEntry === 'string') { + newCommand = firstEntry; + } else { + newCommand = newCommand.substring(1, newCommand.length - 1); + } + } catch { + newCommand = newCommand.substring(1, newCommand.length - 1); + } + } else { + newCommand = newCommand.substring(1, newCommand.length - 1); + } } return newCommand; }