Even more (fish shell this time)

This commit is contained in:
John Richmond 2025-09-29 16:04:04 -07:00
parent 3d737c66c5
commit 04e16f3f1e
2 changed files with 26 additions and 0 deletions

View file

@ -41,6 +41,9 @@ describe("Command Validation", () => {
expect(parseCommand("npm test $(echo test)")).toEqual(["npm test", "echo test"])
expect(parseCommand("npm test `echo test`")).toEqual(["npm test", "echo test"])
expect(parseCommand("diff <(sort f1) <(sort f2)")).toEqual(["diff", "sort f1", "sort f2"])
// fish-style command substitution using parentheses
expect(parseCommand("echo (whoami)")).toEqual(["echo", "whoami"])
expect(parseCommand("echo (echo test)")).toEqual(["echo", "echo test"])
})
it("handles nested backticks with escaped inner backticks", () => {
@ -1097,6 +1100,14 @@ describe("Unified Command Decision Functions", () => {
expect(getCommandDecision("echo `echo \\`whoami\\``", ["echo"], [])).toBe("ask_user")
// And should be auto-denied when inner command is on the denylist
expect(getCommandDecision("echo `echo \\`whoami\\``", ["echo"], ["whoami"])).toBe("auto_deny")
// Fish-style substitutions behave like subshells
expect(getCommandDecision("npm install (echo test)", allowedCommands, deniedCommands)).toBe("auto_approve")
expect(getCommandDecision("npm install (npm test)", allowedCommands, deniedCommands)).toBe("auto_deny")
// Ensure hidden subshell commands are validated even if outer command is allowed
expect(getCommandDecision("echo (whoami)", ["echo", "ls"], [])).toBe("ask_user")
expect(getCommandDecision("echo (whoami)", ["echo", "ls"], ["whoami"])).toBe("auto_deny")
})
it("properly validates subshell commands when no denylist is present", () => {

View file

@ -273,6 +273,21 @@ function parseCommandLine(command: string): string[] {
return `__SUBSH_${subshells.length - 1}__`
})
// Handle fish-style command substitutions and POSIX subshell grouping: ( ... )
// At this point we've already replaced $(), <() and >() earlier, so any remaining (...) are either
// fish command substitutions or subshell groupings. We treat them as subshells to validate inner commands.
processedCommand = processedCommand.replace(/\(([^()]*)\)/g, (full, inner: string, offset: number, str: string) => {
// If this was actually a pattern preceded by $, < or > it would have been handled earlier.
// Guard anyway: if the preceding character indicates a different construct, keep original.
const prevChar = offset > 0 ? str[offset - 1] : ""
if (prevChar === "$" || prevChar === "<" || prevChar === ">") {
return full
}
const content = (inner || "").trim()
if (!content) return full
subshells.push(content)
return `__SUBSH_${subshells.length - 1}__`
})
// Then handle quoted strings
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
quotes.push(match)