feat(terminal): 引号感知的 shell 参数解析与路径去引号纯函数
This commit is contained in:
@@ -467,3 +467,59 @@ export function shellSpawnArgs(configured: string[] = []): string[] {
|
||||
if (configured.length > 0) return [...configured]
|
||||
return process.platform === 'win32' ? [] : ['-l']
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip ONE pair of surrounding quotes from a configured shell path. Users
|
||||
* paste Windows paths with spaces pre-quoted (`"C:\Program Files\…"`); the
|
||||
* quotes are shell-input syntax, not part of the path. Unpaired quotes and
|
||||
* shorter values stay verbatim.
|
||||
*/
|
||||
export function unquotePath(value: string): string {
|
||||
if (value.length >= 2) {
|
||||
const first = value[0]
|
||||
const last = value[value.length - 1]
|
||||
if ((first === '"' && last === '"') || (first === "'" && last === "'")) return value.slice(1, -1)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a settings-page shell-arguments string into argv with quote-aware
|
||||
* grouping: `'…'` / `"…"` group whitespace, and characters inside quotes are
|
||||
* LITERAL — a backslash is never an escape, so Windows paths survive intact
|
||||
* (`-File "C:\my init\init.ps1"` → three tokens, the last containing spaces).
|
||||
* The price is that an argument containing a literal quote character cannot
|
||||
* be expressed; shell startup arguments never need one. An unclosed quote
|
||||
* folds the remainder into the current token (settings input stays
|
||||
* forgiving); an empty quote pair yields no argument.
|
||||
*/
|
||||
export function splitShellArgs(input: string): string[] {
|
||||
const args: string[] = []
|
||||
let current = ''
|
||||
let quote: '"' | "'" | null = null
|
||||
let started = false
|
||||
for (const ch of input) {
|
||||
if (quote !== null) {
|
||||
if (ch === quote) quote = null
|
||||
else current += ch
|
||||
continue
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch
|
||||
started = true
|
||||
continue
|
||||
}
|
||||
if (/\s/.test(ch)) {
|
||||
if (started) {
|
||||
args.push(current)
|
||||
current = ''
|
||||
started = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
current += ch
|
||||
started = true
|
||||
}
|
||||
if (started) args.push(current)
|
||||
return args.filter(arg => arg !== '')
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
resolveShellExecutable,
|
||||
shellDisplayName,
|
||||
shellSpawnArgs,
|
||||
splitShellArgs,
|
||||
unquotePath,
|
||||
} from '../src/pty-manager.ts'
|
||||
|
||||
describe('pty helpers', () => {
|
||||
@@ -156,4 +158,34 @@ describe('pty helpers', () => {
|
||||
expect(helper).toBeTruthy()
|
||||
expect((statSync(helper!).mode & 0o111) !== 0).toBe(true)
|
||||
})
|
||||
|
||||
it('unquotes a paired surrounding quote from a configured shell path', () => {
|
||||
expect(unquotePath('"C:\\Program Files\\PowerShell\\7\\pwsh.exe"'))
|
||||
.toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
|
||||
expect(unquotePath("'/usr/bin/my shell'")).toBe('/usr/bin/my shell')
|
||||
// Unpaired or single characters stay verbatim.
|
||||
expect(unquotePath('"mismatched')).toBe('"mismatched')
|
||||
expect(unquotePath('pwsh.exe')).toBe('pwsh.exe')
|
||||
expect(unquotePath('"')).toBe('"')
|
||||
expect(unquotePath('')).toBe('')
|
||||
})
|
||||
|
||||
it('splits shell args with quote-aware grouping', () => {
|
||||
expect(splitShellArgs('-NoLogo -File "C:\\my init\\init.ps1"'))
|
||||
.toEqual(['-NoLogo', '-File', 'C:\\my init\\init.ps1'])
|
||||
// Single quotes group too, and preserve inner double quotes verbatim.
|
||||
expect(splitShellArgs("-c 'echo \"hi\"'")).toEqual(['-c', 'echo "hi"'])
|
||||
expect(splitShellArgs(' -l ')).toEqual(['-l'])
|
||||
expect(splitShellArgs('')).toEqual([])
|
||||
expect(splitShellArgs(' ')).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps backslashes literal inside quotes and tolerates an unclosed quote', () => {
|
||||
// Backslash is NOT an escape (Windows paths): "C:\a\" ends with a slash.
|
||||
expect(splitShellArgs('"C:\\a\\"')).toEqual(['C:\\a\\'])
|
||||
// An unclosed quote folds the remainder into one token instead of erroring.
|
||||
expect(splitShellArgs('"unclosed quote')).toEqual(['unclosed quote'])
|
||||
// Empty quote pairs produce no empty-string argument.
|
||||
expect(splitShellArgs('"" x')).toEqual(['x'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user