Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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 : '<default>'}`)
const cmdProcess = spawn(fullCmd, [], { cwd: newProjectPath, shell })

let err = ''
cmdProcess.stderr.on('data', data => err += data.toString())
Expand Down
29 changes: 29 additions & 0 deletions src/shell.ts
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 32 additions & 3 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})