mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: resolve 'Bad substitution' error in command parsing (#5743)
This commit is contained in:
parent
1c26bbc5ab
commit
b0820ac755
2 changed files with 194 additions and 5 deletions
|
|
@ -276,6 +276,123 @@ done`
|
|||
parseCommand(problematicPart)
|
||||
}).not.toThrow("Bad substitution")
|
||||
})
|
||||
|
||||
it("should handle bash arithmetic expressions with $(())", () => {
|
||||
// Test the exact script from the user's error
|
||||
const bashScript = `jsx_files=$(find resources/js -name "*.jsx" -type f -not -path "*/node_modules/*")
|
||||
count=0
|
||||
for file in $jsx_files; do
|
||||
ts_file="\${file%.jsx}.tsx"
|
||||
if [ ! -f "$ts_file" ]; then
|
||||
cp "$file" "$ts_file"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done
|
||||
echo "Successfully converted $count .jsx files to .tsx"`
|
||||
|
||||
expect(() => {
|
||||
parseCommand(bashScript)
|
||||
}).not.toThrow("Bad substitution: calc.add")
|
||||
})
|
||||
|
||||
it("should correctly parse commands with arithmetic expressions", () => {
|
||||
const result = parseCommand("count=$((count + 1)) && echo $count")
|
||||
expect(result).toEqual(["count=$((count + 1))", "echo $count"])
|
||||
})
|
||||
|
||||
it("should handle nested arithmetic expressions", () => {
|
||||
const result = parseCommand("result=$((10 * (5 + 3))) && echo $result")
|
||||
expect(result).toEqual(["result=$((10 * (5 + 3)))", "echo $result"])
|
||||
})
|
||||
|
||||
it("should handle arithmetic expressions with variables", () => {
|
||||
const result = parseCommand("total=$((price * quantity + tax))")
|
||||
expect(result).toEqual(["total=$((price * quantity + tax))"])
|
||||
})
|
||||
|
||||
it("should handle complex parameter expansions without errors", () => {
|
||||
const commands = [
|
||||
"echo ${var:-default}",
|
||||
"echo ${#array[@]}",
|
||||
"echo ${var%suffix}",
|
||||
"echo ${var#prefix}",
|
||||
"echo ${var/pattern/replacement}",
|
||||
"echo ${!var}",
|
||||
"echo ${var:0:5}",
|
||||
"echo ${var,,}",
|
||||
"echo ${var^^}",
|
||||
]
|
||||
|
||||
commands.forEach((cmd) => {
|
||||
expect(() => {
|
||||
parseCommand(cmd)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle process substitutions without errors", () => {
|
||||
const commands = [
|
||||
"diff <(sort file1) <(sort file2)",
|
||||
"command >(gzip > output.gz)",
|
||||
"while read line; do echo $line; done < <(cat file)",
|
||||
]
|
||||
|
||||
commands.forEach((cmd) => {
|
||||
expect(() => {
|
||||
parseCommand(cmd)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle special bash variables without errors", () => {
|
||||
const commands = [
|
||||
"echo $?",
|
||||
"echo $!",
|
||||
"echo $#",
|
||||
"echo $$",
|
||||
"echo $@",
|
||||
"echo $*",
|
||||
"echo $-",
|
||||
"echo $0",
|
||||
"echo $1 $2 $3",
|
||||
]
|
||||
|
||||
commands.forEach((cmd) => {
|
||||
expect(() => {
|
||||
parseCommand(cmd)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle mixed complex bash constructs", () => {
|
||||
const complexCommand = `
|
||||
for file in \${files[@]}; do
|
||||
if [[ -f "\${file%.txt}.bak" ]]; then
|
||||
count=\$((count + 1))
|
||||
echo "Processing \${file} (\$count/\${#files[@]})"
|
||||
result=\$(process_file "\$file" 2>&1)
|
||||
if [[ \$? -eq 0 ]]; then
|
||||
echo "Success: \$result" >(logger)
|
||||
fi
|
||||
fi
|
||||
done
|
||||
`
|
||||
|
||||
expect(() => {
|
||||
parseCommand(complexCommand)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle fallback parsing when shell-quote fails", () => {
|
||||
// Test a command that might cause shell-quote to fail
|
||||
const problematicCommand = "echo ${unclosed"
|
||||
|
||||
expect(() => {
|
||||
const result = parseCommand(problematicCommand)
|
||||
// Should not throw and should return some result
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("isAutoApprovedCommand (legacy behavior)", () => {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ export function parseCommand(command: string): string[] {
|
|||
const subshells: string[] = []
|
||||
const quotes: string[] = []
|
||||
const arrayIndexing: string[] = []
|
||||
const arithmeticExpressions: string[] = []
|
||||
const variables: string[] = []
|
||||
const parameterExpansions: string[] = []
|
||||
const processSubstitutions: string[] = []
|
||||
|
||||
// First handle PowerShell redirections by temporarily replacing them
|
||||
let processedCommand = command.replace(/\d*>&\d*/g, (match) => {
|
||||
|
|
@ -83,10 +87,37 @@ export function parseCommand(command: string): string[] {
|
|||
return `__REDIR_${redirections.length - 1}__`
|
||||
})
|
||||
|
||||
// Handle array indexing expressions: ${array[...]} pattern and partial expressions
|
||||
processedCommand = processedCommand.replace(/\$\{[^}]*\[[^\]]*(\]([^}]*\})?)?/g, (match) => {
|
||||
arrayIndexing.push(match)
|
||||
return `__ARRAY_${arrayIndexing.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}__`
|
||||
})
|
||||
|
||||
// Handle parameter expansions: ${...} patterns (including array indexing)
|
||||
// This covers ${var}, ${var:-default}, ${var:+alt}, ${#var}, ${var%pattern}, etc.
|
||||
processedCommand = processedCommand.replace(/\$\{[^}]+\}/g, (match) => {
|
||||
parameterExpansions.push(match)
|
||||
return `__PARAM_${parameterExpansions.length - 1}__`
|
||||
})
|
||||
|
||||
// Handle process substitutions: <(...) and >(...)
|
||||
processedCommand = processedCommand.replace(/[<>]\([^)]+\)/g, (match) => {
|
||||
processSubstitutions.push(match)
|
||||
return `__PROCSUB_${processSubstitutions.length - 1}__`
|
||||
})
|
||||
|
||||
// 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}__`
|
||||
})
|
||||
|
||||
// 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
|
||||
|
|
@ -106,7 +137,40 @@ export function parseCommand(command: string): string[] {
|
|||
return `__QUOTE_${quotes.length - 1}__`
|
||||
})
|
||||
|
||||
const tokens = parse(processedCommand) as ShellToken[]
|
||||
let tokens: ShellToken[]
|
||||
try {
|
||||
tokens = parse(processedCommand) as ShellToken[]
|
||||
} catch (error: any) {
|
||||
// If shell-quote fails to parse, fall back to simple splitting
|
||||
console.warn("shell-quote parse error:", error.message, "for command:", processedCommand)
|
||||
|
||||
// Simple fallback: split by common operators
|
||||
const fallbackCommands = processedCommand
|
||||
.split(/(?:&&|\|\||;|\|)/)
|
||||
.map((cmd) => cmd.trim())
|
||||
.filter((cmd) => cmd.length > 0)
|
||||
|
||||
// Restore all placeholders for each command
|
||||
return fallbackCommands.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)])
|
||||
// Restore arithmetic expressions
|
||||
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
|
||||
// Restore parameter expansions
|
||||
result = result.replace(/__PARAM_(\d+)__/g, (_, i) => parameterExpansions[parseInt(i)])
|
||||
// Restore process substitutions
|
||||
result = result.replace(/__PROCSUB_(\d+)__/g, (_, i) => processSubstitutions[parseInt(i)])
|
||||
// Restore variable references
|
||||
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const commands: string[] = []
|
||||
let currentCommand: string[] = []
|
||||
|
||||
|
|
@ -151,6 +215,14 @@ export function parseCommand(command: string): string[] {
|
|||
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
|
||||
// Restore array indexing expressions
|
||||
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
|
||||
// Restore arithmetic expressions
|
||||
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
|
||||
// Restore parameter expansions
|
||||
result = result.replace(/__PARAM_(\d+)__/g, (_, i) => parameterExpansions[parseInt(i)])
|
||||
// Restore process substitutions
|
||||
result = result.replace(/__PROCSUB_(\d+)__/g, (_, i) => processSubstitutions[parseInt(i)])
|
||||
// Restore variable references
|
||||
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue