Files
Claude-Code-Workflow/ccw/src/utils/shell-escape.ts
catlog22 eea859dd6f fix(cli): 修复 Windows 路径反斜杠被吞掉的问题并添加跨平台路径支持
- 重写 escapeWindowsArg 函数,正确处理反斜杠和引号转义
- 添加 escapeUnixArg 函数支持 Linux/macOS shell 转义
- 添加 normalizePathSeparators 函数自动转换路径分隔符
- 修复 vscode-lsp.ts 中的 TypeScript 类型错误
2026-01-20 09:44:49 +08:00

118 lines
3.6 KiB
TypeScript

/**
* Cross-platform shell argument escaping utilities.
*
* Provides proper escaping for command-line arguments when using spawn({ shell: true })
* on different platforms:
* - Windows: cmd.exe metacharacter handling with proper quote escaping
* - Unix (Linux/macOS): POSIX shell escaping with single quotes
*/
/**
* Escape a command-line argument for Windows cmd.exe.
* Follows Microsoft C/C++ runtime argv parsing rules.
*
* Rules:
* 1. Arguments containing spaces, tabs, or quotes must be quoted
* 2. Backslashes are literal unless followed by a quote
* 3. To include a literal quote, use \"
* 4. Backslashes before a quote must be doubled: \\" produces \"
* 5. Trailing backslashes must be doubled when the arg is quoted
*
* @param arg - The argument to escape
* @returns Properly escaped argument safe for Windows command line
*/
export function escapeWindowsArg(arg: string): string {
if (arg === '') return '""';
// Normalize newlines to spaces to prevent cmd.exe issues
arg = arg.replace(/\r?\n/g, ' ');
// Check if the argument needs quoting
// Characters that require quoting: space, tab, double quote, or backslash
const needsQuoting = /[ \t"\\]/.test(arg);
if (!needsQuoting) {
// No special characters - return as is
return arg;
}
// Build the escaped string with proper quoting
let result = '"';
for (let i = 0; i < arg.length; i++) {
// Count consecutive backslashes
let backslashes = 0;
while (i < arg.length && arg[i] === '\\') {
backslashes++;
i++;
}
if (i === arg.length) {
// Trailing backslashes: double them all (they precede the closing quote)
result += '\\'.repeat(backslashes * 2);
break;
}
if (arg[i] === '"') {
// Backslashes followed by a quote: double the backslashes and escape the quote
result += '\\'.repeat(backslashes * 2 + 1) + '"';
} else {
// Backslashes not followed by a quote: they are literal
result += '\\'.repeat(backslashes) + arg[i];
}
}
result += '"';
return result;
}
/**
* Escape a command-line argument for Unix shells (bash, sh, zsh).
* Uses single quotes which treat all characters literally except single quote itself.
*
* Strategy:
* - Wrap argument in single quotes
* - Replace any single quotes with: '\'' (end quote, escaped quote, start quote)
*
* @param arg - The argument to escape
* @returns Properly escaped argument safe for Unix shell
*/
export function escapeUnixArg(arg: string): string {
if (arg === '') return "''";
// If no special characters, return as-is
// Safe characters: alphanumeric, hyphen, underscore, dot, forward slash, colon
if (/^[a-zA-Z0-9_.\/:@=-]+$/.test(arg)) {
return arg;
}
// Use single quotes and escape any single quotes within
// 'arg' -> 'arg'\''s' for "arg's"
return "'" + arg.replace(/'/g, "'\\''") + "'";
}
/**
* Escape a command-line argument for the current platform.
* Automatically selects the appropriate escaping method based on OS.
*
* @param arg - The argument to escape
* @returns Properly escaped argument for the current platform's shell
*/
export function escapeShellArg(arg: string): string {
if (process.platform === 'win32') {
return escapeWindowsArg(arg);
}
return escapeUnixArg(arg);
}
/**
* Check if we need shell escaping on the current platform.
* Windows always needs escaping when shell: true due to cmd.exe.
* Unix typically doesn't need escaping when shell: false.
*
* @returns true if shell escaping should be applied
*/
export function needsShellEscaping(): boolean {
return process.platform === 'win32';
}