From b808cedff3a907eda4f3e1b5287dc4fdd3f0f415 Mon Sep 17 00:00:00 2001 From: giankpetrov <69126856+giankpetrov@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:30:21 +0000 Subject: [PATCH] test: improve testing of shell escaping logic Refactored `escapeForTerminal` in `src/utils/shell.ts` to extract the core logic into a pure function `escapeTextForShell` inside a new file `src/utils/shellEscape.ts`. This bypasses VS Code module loading errors in the native node test runner while allowing full test coverage. Added test coverage in `src/utils/shell.test.ts` for PowerShell, Windows CMD, and POSIX shell escaping logic. --- src/utils/shell.test.ts | 17 +++++++++++++++++ src/utils/shell.ts | 22 +++------------------- src/utils/shellEscape.ts | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 src/utils/shell.test.ts create mode 100644 src/utils/shellEscape.ts diff --git a/src/utils/shell.test.ts b/src/utils/shell.test.ts new file mode 100644 index 0000000..c7a7ec2 --- /dev/null +++ b/src/utils/shell.test.ts @@ -0,0 +1,17 @@ +import * as assert from "node:assert"; +import { test } from "node:test"; +import { escapeTextForShell } from "./shellEscape.ts"; + +test("escapeTextForShell", () => { + // PowerShell + assert.strictEqual(escapeTextForShell("hello", "powershell"), "'hello'"); + assert.strictEqual(escapeTextForShell("hello 'world'", "pwsh"), "'hello ''world'''"); + + // Windows CMD + assert.strictEqual(escapeTextForShell("hello", "cmd.exe"), "\"hello\""); + assert.strictEqual(escapeTextForShell("hello \"world\"", "cmd.exe"), "\"hello \"\"world\"\"\""); + + // POSIX + assert.strictEqual(escapeTextForShell("hello", "bash"), "'hello'"); + assert.strictEqual(escapeTextForShell("hello 'world'", "zsh"), "'hello '\\''world'\\'''"); +}); diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 5a0b5c4..d37646b 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -1,4 +1,6 @@ +import type { ExtensionContext } from "vscode"; import * as vscode from "vscode"; +import { escapeTextForShell } from "./shellEscape"; /** * Escapes a string to be safely used as a command line argument @@ -7,23 +9,5 @@ import * as vscode from "vscode"; export function escapeForTerminal(text: string): string { // Try to determine the shell from VS Code environment const shell = (vscode.env.shell || "").toLowerCase(); - - // PowerShell - if (shell.includes("powershell") || shell.includes("pwsh")) { - // Wrap in single quotes, escape literal single quotes by doubling them - return `'${text.replace(/'/g, "''")}'`; - } - - // Windows CMD - if (shell.includes("cmd.exe")) { - // CMD is notoriously difficult to escape perfectly. - // We replace double quotes with escaped double quotes and wrap in double quotes. - // For highly complex strings (like code diffs), CMD might still struggle - // with %, !, ^, etc., but this prevents trivial command injection via " & ". - return `"${text.replace(/"/g, '""')}"`; - } - - // Default to POSIX (Bash, Zsh, Git Bash, macOS, Linux) - // Wrap in single quotes, escape literal single quotes as '\'' - return `'${text.replace(/'/g, "'\\''")}'`; + return escapeTextForShell(text, shell); } diff --git a/src/utils/shellEscape.ts b/src/utils/shellEscape.ts new file mode 100644 index 0000000..ee36115 --- /dev/null +++ b/src/utils/shellEscape.ts @@ -0,0 +1,24 @@ +/** + * Escapes a string to be safely used as a command line argument + * in a specified shell. + */ +export function escapeTextForShell(text: string, shell: string): string { + // PowerShell + if (shell.includes("powershell") || shell.includes("pwsh")) { + // Wrap in single quotes, escape literal single quotes by doubling them + return `'${text.replace(/'/g, "''")}'`; + } + + // Windows CMD + if (shell.includes("cmd.exe")) { + // CMD is notoriously difficult to escape perfectly. + // We replace double quotes with escaped double quotes and wrap in double quotes. + // For highly complex strings (like code diffs), CMD might still struggle + // with %, !, ^, etc., but this prevents trivial command injection via " & ". + return `"${text.replace(/"/g, '""')}"`; + } + + // Default to POSIX (Bash, Zsh, Git Bash, macOS, Linux) + // Wrap in single quotes, escape literal single quotes as '\'' + return `'${text.replace(/'/g, "'\\''")}'`; +}