diff --git a/packages/core/src/utils/shell-utils.test.ts b/packages/core/src/utils/shell-utils.test.ts index 25c2dc5593..e330e251e8 100644 --- a/packages/core/src/utils/shell-utils.test.ts +++ b/packages/core/src/utils/shell-utils.test.ts @@ -609,7 +609,13 @@ describe('detectSelfKillCommand', () => { it('detects kill commands using pgrep selectors for qwen-code hosts', () => { expect(detectSelfKillCommand('kill -9 $(pgrep node)')).toBe(true); expect(detectSelfKillCommand('kill $(pgrep -f node)')).toBe(true); + expect(detectSelfKillCommand('kill -9 $(pgrep node | head -1)')).toBe(true); + expect(detectSelfKillCommand('kill -9 `pgrep node | head -1`')).toBe(true); expect(detectSelfKillCommand('pgrep node | xargs kill')).toBe(true); + expect(detectSelfKillCommand('pgrep node | xargs sudo kill')).toBe(true); + expect(detectSelfKillCommand('pgrep node | xargs -I {} kill -9 {}')).toBe( + true, + ); }); it('detects taskkill inline and dash-prefixed image options', () => { diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index 0fba60c995..6e5476fb97 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -208,6 +208,8 @@ export function splitCommands(command: string): string[] { let currentCommand = ''; let inSingleQuotes = false; let inDoubleQuotes = false; + let inBackticks = false; + let substitutionDepth = 0; let i = 0; const previousNonWhitespaceChar = (index: number): string | undefined => { @@ -235,13 +237,42 @@ export function splitCommands(command: string): string[] { continue; } - if (char === "'" && !inDoubleQuotes) { + if (!inSingleQuotes && char === '`') { + inBackticks = !inBackticks; + } else if ( + !inSingleQuotes && + !inBackticks && + char === '$' && + nextChar === '(' + ) { + substitutionDepth++; + currentCommand += '$('; + i += 2; + continue; + } else if (!inBackticks && substitutionDepth > 0 && char === ')') { + substitutionDepth--; + } else if ( + !inBackticks && + substitutionDepth === 0 && + char === "'" && + !inDoubleQuotes + ) { inSingleQuotes = !inSingleQuotes; - } else if (char === '"' && !inSingleQuotes) { + } else if ( + !inBackticks && + substitutionDepth === 0 && + char === '"' && + !inSingleQuotes + ) { inDoubleQuotes = !inDoubleQuotes; } - if (!inSingleQuotes && !inDoubleQuotes) { + if ( + !inSingleQuotes && + !inDoubleQuotes && + !inBackticks && + substitutionDepth === 0 + ) { if ( (char === '&' && nextChar === '&') || (char === '|' && (nextChar === '|' || nextChar === '&')) @@ -460,6 +491,28 @@ const PKILL_OPTIONS_WITH_VALUES = new Set([ '--euid', ]); +const XARGS_OPTIONS_WITH_VALUES = new Set([ + '-I', + '-n', + '-P', + '-s', + '-E', + '-d', + '-L', + '-l', + '-a', + '-J', + '-R', + '--replace', + '--max-args', + '--max-procs', + '--max-chars', + '--eof', + '--delimiter', + '--max-lines', + '--arg-file', +]); + export const SHELL_SELF_KILL_REJECTION = 'Blocked: this command may terminate the running qwen-code process because it targets all node/qwen-code processes. Use task_stop for managed background shells, or kill a specific PID instead.'; @@ -773,6 +826,34 @@ function pgrepSubstitutionArgs(segment: string): string[] { continue; } + if (char === '`') { + let end = index + 1; + let innerEscaped = false; + for (; end < segment.length; end++) { + const innerChar = segment[end]!; + if (innerEscaped) { + innerEscaped = false; + continue; + } + if (innerChar === '\\') { + innerEscaped = true; + continue; + } + if (innerChar === '`') break; + } + if (end >= segment.length) { + break; + } + + const inner = segment.slice(index + 1, end).trim(); + const match = inner.match(/^pgrep\b(.*)$/i); + if (match) { + args.push(match[1] ?? ''); + } + index = end; + continue; + } + if (char !== '$' || segment[index + 1] !== '(') { continue; } @@ -813,7 +894,10 @@ function xargsInvokesKill(segment: string | undefined): boolean { return false; } - const command = tokens.slice(1).find((token) => !isOptionToken(token)); + const commandTokens = unwrapExecutionPrefixes( + commandArguments(tokens, XARGS_OPTIONS_WITH_VALUES), + ); + const command = commandTokens.find((token) => !isOptionToken(token)); return normalizeExecutableName(command ?? '') === 'kill'; }