diff --git a/src/commands.ts b/src/commands.ts index 57b522b..e604458 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -13,6 +13,7 @@ import { addTraceFile, getWorkspacePath, openTerminal, openTraceDirectoryExterna import { addTraceDiagnostics, clearTaceDiagnostics } from './traceDiagnostics' import { setStatusBarState } from './statusBar' import { afterWatches, projectPath, saveName, state, traceFiles, traceRunning } from './appState' +import { buildTraceCommand, resolveTraceShell } from './shell' const readdir = promisify(readdirC) @@ -128,9 +129,11 @@ async function runTrace(args?: unknown[]) { return } - const quotedTraceDir = `'${traceDir}'` - // eslint-disable-next-line no-template-curly-in-string - const fullCmd = `(cd '${newDirName ?? workspacePath}'; ${traceCmd.replace('${traceDir}', quotedTraceDir)})` + const shell = resolveTraceShell(process.env.SHELL) + const fullCmd = buildTraceCommand(traceCmd, traceDir, { + platform: process.platform, + shell: typeof shell === 'string' ? shell : undefined, + }) log(fullCmd) @@ -147,8 +150,8 @@ async function runTrace(args?: unknown[]) { setStatusBarState('traceError', false) - log(`shell: ${process.env.SHELL}`) - const cmdProcess = spawn(fullCmd, [], { cwd: newProjectPath, shell: process.env.SHELL }) + log(`shell: ${typeof shell === 'string' ? shell : ''}`) + const cmdProcess = spawn(fullCmd, [], { cwd: newProjectPath, shell }) let err = '' cmdProcess.stderr.on('data', data => err += data.toString()) diff --git a/src/shell.ts b/src/shell.ts new file mode 100644 index 0000000..f245e09 --- /dev/null +++ b/src/shell.ts @@ -0,0 +1,29 @@ +import process from 'node:process' + +const posixShellPattern = /(?:^|[\\/])(?:bash|dash|fish|ksh|sh|zsh)(?:\.exe)?$/i +const traceDirPlaceholder = '${' + 'traceDir}' + +export function shouldUsePosixQuoting(platform: NodeJS.Platform, shell?: string) { + if (shell && posixShellPattern.test(shell)) + return true + + return platform !== 'win32' +} + +export function quoteForShell(value: string, opts?: { platform?: NodeJS.Platform, shell?: string }) { + const platform = opts?.platform ?? process.platform + const shell = opts?.shell + + if (!shouldUsePosixQuoting(platform, shell)) + return `"${value.replace(/"/g, '""')}"` + + return `'${value.replace(/'/g, `'\\''`)}'` +} + +export function buildTraceCommand(traceCmd: string, traceDir: string, opts?: { platform?: NodeJS.Platform, shell?: string }) { + return traceCmd.replace(traceDirPlaceholder, quoteForShell(traceDir, opts)) +} + +export function resolveTraceShell(shell?: string): string | true { + return shell || true +} diff --git a/test/index.test.ts b/test/index.test.ts index 401553c..00e8c71 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,36 @@ import { describe, expect, it } from 'vitest' +import { buildTraceCommand, quoteForShell, resolveTraceShell, shouldUsePosixQuoting } from '../src/shell' -describe('should', () => { - it('exported', () => { - expect(1).toEqual(1) +describe('shell helpers', () => { + const traceCmd = 'npx tsc --generateTrace ' + '${' + 'traceDir}' + + it('uses default Windows quoting when no explicit shell is configured', () => { + expect(buildTraceCommand(traceCmd, 'C:\\Users\\me\\trace dir', { platform: 'win32' })) + .toBe('npx tsc --generateTrace "C:\\Users\\me\\trace dir"') + }) + + it('uses POSIX quoting when the configured shell is bash on Windows', () => { + expect(buildTraceCommand(traceCmd, 'C:\\Users\\me\\trace dir', { + platform: 'win32', + shell: 'C:\\Program Files\\Git\\bin\\bash.exe', + })).toBe(`npx tsc --generateTrace 'C:\\Users\\me\\trace dir'`) + }) + + it('escapes single quotes for POSIX shells', () => { + expect(quoteForShell(`/tmp/it's here`, { platform: 'linux' })).toBe(`'/tmp/it'\\''s here'`) + }) + + it('falls back to the platform default shell when SHELL is absent', () => { + expect(resolveTraceShell()).toBe(true) + }) + + it('keeps an explicit shell path when provided', () => { + expect(resolveTraceShell('/bin/zsh')).toBe('/bin/zsh') + }) + + it('detects POSIX shells from the shell executable path', () => { + expect(shouldUsePosixQuoting('win32', 'C:\\Program Files\\Git\\bin\\bash.exe')).toBe(true) + expect(shouldUsePosixQuoting('linux')).toBe(true) + expect(shouldUsePosixQuoting('win32')).toBe(false) }) })