mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
fix: update command parser to handle all edge cases from original implementation
- Add support for all line ending types (\r\n, \r, \n) - Handle simple variable references (, ) to prevent shell-quote from splitting them - Handle special bash variables (0, , etc.) - Maintain compatibility with all existing tests - Fix linting warnings
This commit is contained in:
parent
55d0ff3f69
commit
17dbda3d4d
1 changed files with 151 additions and 77 deletions
|
|
@ -27,97 +27,171 @@ export function parseCommandString(command: string): {
|
|||
}
|
||||
}
|
||||
|
||||
// Storage for replaced content
|
||||
const redirections: string[] = []
|
||||
const subshells: string[] = []
|
||||
const quotes: string[] = []
|
||||
const arrayIndexing: string[] = []
|
||||
try {
|
||||
// First split by newlines (including all types: \n, \r\n, \r) to handle multi-line commands
|
||||
const lines = command.split(/\r\n|\r|\n/)
|
||||
const allCommands: string[] = []
|
||||
|
||||
// First handle PowerShell redirections by temporarily replacing them
|
||||
let processedCommand = command.replace(/\d*>&\d*/g, (match) => {
|
||||
redirections.push(match)
|
||||
return `__REDIR_${redirections.length - 1}__`
|
||||
})
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (!trimmedLine) continue // Skip empty lines
|
||||
|
||||
// Handle array indexing expressions: ${array[...]} pattern and partial expressions
|
||||
processedCommand = processedCommand.replace(/\$\{[^}]*\[[^\]]*(\]([^}]*\})?)?/g, (match) => {
|
||||
arrayIndexing.push(match)
|
||||
return `__ARRAY_${arrayIndexing.length - 1}__`
|
||||
})
|
||||
// Storage for replaced content
|
||||
const redirections: string[] = []
|
||||
const subshells: string[] = []
|
||||
const quotes: string[] = []
|
||||
const arrayIndexing: string[] = []
|
||||
const arithmeticExpressions: string[] = []
|
||||
const variables: string[] = []
|
||||
|
||||
// Then handle subshell commands - store them for security analysis
|
||||
const hasSubshells = command.includes("$(") || command.includes("`")
|
||||
// First handle PowerShell redirections by temporarily replacing them
|
||||
let processedCommand = trimmedLine.replace(/\d*>&\d*/g, (match) => {
|
||||
redirections.push(match)
|
||||
return `__REDIR_${redirections.length - 1}__`
|
||||
})
|
||||
|
||||
processedCommand = processedCommand
|
||||
.replace(/\$\((.*?)\)/g, (_, inner) => {
|
||||
const trimmedInner = inner.trim()
|
||||
subshells.push(trimmedInner)
|
||||
return `__SUBSH_${subshells.length - 1}__`
|
||||
})
|
||||
.replace(/`(.*?)`/g, (_, inner) => {
|
||||
const trimmedInner = inner.trim()
|
||||
subshells.push(trimmedInner)
|
||||
return `__SUBSH_${subshells.length - 1}__`
|
||||
})
|
||||
// Handle arithmetic expressions: $((...)) pattern
|
||||
// Match the entire arithmetic expression including nested parentheses
|
||||
processedCommand = processedCommand.replace(/\$\(\([^)]*(?:\)[^)]*)*\)\)/g, (match) => {
|
||||
arithmeticExpressions.push(match)
|
||||
return `__ARITH_${arithmeticExpressions.length - 1}__`
|
||||
})
|
||||
|
||||
// Then handle quoted strings
|
||||
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
|
||||
quotes.push(match)
|
||||
return `__QUOTE_${quotes.length - 1}__`
|
||||
})
|
||||
// Handle array indexing expressions: ${array[...]} pattern and partial expressions
|
||||
processedCommand = processedCommand.replace(/\$\{[^}]*\[[^\]]*(\]([^}]*\})?)?/g, (match) => {
|
||||
arrayIndexing.push(match)
|
||||
return `__ARRAY_${arrayIndexing.length - 1}__`
|
||||
})
|
||||
|
||||
const tokens = parse(processedCommand) as ShellToken[]
|
||||
const commands: string[] = []
|
||||
let currentCommand: string[] = []
|
||||
// Handle simple variable references: $varname pattern
|
||||
// This prevents shell-quote from splitting $count into separate tokens
|
||||
processedCommand = processedCommand.replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, (match) => {
|
||||
variables.push(match)
|
||||
return `__VAR_${variables.length - 1}__`
|
||||
})
|
||||
|
||||
for (const token of tokens) {
|
||||
if (typeof token === "object" && "op" in token) {
|
||||
// Chain operator - split command
|
||||
if (["&&", "||", ";", "|"].includes(token.op)) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
// Handle special bash variables: $?, $!, $#, $$, $@, $*, $-, $0-$9
|
||||
processedCommand = processedCommand.replace(/\$[?!#$@*\-0-9]/g, (match) => {
|
||||
variables.push(match)
|
||||
return `__VAR_${variables.length - 1}__`
|
||||
})
|
||||
|
||||
// Then handle subshell commands - store them for security analysis
|
||||
const _hasSubshells = trimmedLine.includes("$(") || trimmedLine.includes("`")
|
||||
|
||||
processedCommand = processedCommand
|
||||
.replace(/\$\(((?!\().*?)\)/g, (_, inner) => {
|
||||
const trimmedInner = inner.trim()
|
||||
subshells.push(trimmedInner)
|
||||
return `__SUBSH_${subshells.length - 1}__`
|
||||
})
|
||||
.replace(/`(.*?)`/g, (_, inner) => {
|
||||
const trimmedInner = inner.trim()
|
||||
subshells.push(trimmedInner)
|
||||
return `__SUBSH_${subshells.length - 1}__`
|
||||
})
|
||||
|
||||
// Then handle quoted strings
|
||||
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
|
||||
quotes.push(match)
|
||||
return `__QUOTE_${quotes.length - 1}__`
|
||||
})
|
||||
|
||||
const tokens = parse(processedCommand) as ShellToken[]
|
||||
const commands: string[] = []
|
||||
let currentCommand: string[] = []
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]
|
||||
|
||||
if (typeof token === "object" && "op" in token) {
|
||||
// Chain operator - split command
|
||||
if (["&&", "||", ";", "|"].includes(token.op)) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
} else {
|
||||
// Other operators (>, &) are part of the command
|
||||
currentCommand.push(token.op)
|
||||
}
|
||||
} else if (typeof token === "string") {
|
||||
// Check if it's a subshell placeholder
|
||||
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
|
||||
if (subshellMatch) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
commands.push(subshells[parseInt(subshellMatch[1])])
|
||||
} else {
|
||||
currentCommand.push(token)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Other operators (>, &) are part of the command
|
||||
currentCommand.push(token.op)
|
||||
}
|
||||
} else if (typeof token === "string") {
|
||||
// Check if it's a subshell placeholder
|
||||
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
|
||||
if (subshellMatch) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
commands.push(subshells[parseInt(subshellMatch[1])])
|
||||
} else {
|
||||
currentCommand.push(token)
|
||||
|
||||
// Add any remaining command
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
}
|
||||
|
||||
// Restore quotes, redirections, arithmetic expressions, variables, and array indexing
|
||||
const restoredCommands = commands.map((cmd) => {
|
||||
let result = cmd
|
||||
// Restore quotes
|
||||
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
|
||||
// Restore redirections
|
||||
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
|
||||
// Restore arithmetic expressions
|
||||
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
|
||||
// Restore variables
|
||||
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
|
||||
// Restore array indexing expressions
|
||||
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
|
||||
return result
|
||||
})
|
||||
|
||||
allCommands.push(...restoredCommands)
|
||||
}
|
||||
|
||||
// Check if any line has subshells
|
||||
const hasSubshells = command.includes("$(") || command.includes("`")
|
||||
const subshellCommands: string[] = []
|
||||
|
||||
// Extract subshell commands for security analysis
|
||||
let match: RegExpExecArray | null
|
||||
const subshellRegex1 = /\$\(((?!\().*?)\)/g
|
||||
const subshellRegex2 = /`(.*?)`/g
|
||||
|
||||
while ((match = subshellRegex1.exec(command)) !== null) {
|
||||
if (match[1]) {
|
||||
subshellCommands.push(match[1].trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining command
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
}
|
||||
while ((match = subshellRegex2.exec(command)) !== null) {
|
||||
if (match[1]) {
|
||||
subshellCommands.push(match[1].trim())
|
||||
}
|
||||
}
|
||||
|
||||
// Restore quotes, redirections, and array indexing
|
||||
const restoredCommands = commands.map((cmd) => {
|
||||
let result = cmd
|
||||
// Restore quotes
|
||||
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
|
||||
// Restore redirections
|
||||
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
|
||||
// Restore array indexing expressions
|
||||
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
|
||||
return result
|
||||
})
|
||||
return {
|
||||
subCommands: allCommands,
|
||||
hasSubshells,
|
||||
subshellCommands,
|
||||
}
|
||||
} catch (_error) {
|
||||
// If shell-quote fails, fall back to simple splitting
|
||||
const fallbackCommands = command
|
||||
.split(/\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
||||
return {
|
||||
subCommands: restoredCommands,
|
||||
hasSubshells,
|
||||
subshellCommands: subshells,
|
||||
return {
|
||||
subCommands: fallbackCommands.length > 0 ? fallbackCommands : [command],
|
||||
hasSubshells: command.includes("$(") || command.includes("`"),
|
||||
subshellCommands: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue