Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions webview-ui/src/utils/__tests__/command-validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,121 @@ describe("Command Validation", () => {
parseCommand('npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"'),
).toEqual(["npm test", 'Select-String -NotMatch "node_modules"', 'Select-String "FAIL|Error"'])
})

describe("newline handling", () => {
it("splits commands by Unix newlines (\\n)", () => {
expect(parseCommand("echo hello\ngit status\nnpm install")).toEqual([
"echo hello",
"git status",
"npm install",
])
})

it("splits commands by Windows newlines (\\r\\n)", () => {
expect(parseCommand("echo hello\r\ngit status\r\nnpm install")).toEqual([
"echo hello",
"git status",
"npm install",
])
})

it("splits commands by old Mac newlines (\\r)", () => {
expect(parseCommand("echo hello\rgit status\rnpm install")).toEqual([
"echo hello",
"git status",
"npm install",
])
})

it("handles mixed line endings", () => {
expect(parseCommand("echo hello\ngit status\r\nnpm install\rls -la")).toEqual([
"echo hello",
"git status",
"npm install",
"ls -la",
])
})

it("ignores empty lines", () => {
expect(parseCommand("echo hello\n\n\ngit status\r\n\r\nnpm install")).toEqual([
"echo hello",
"git status",
"npm install",
])
})

it("handles newlines with chain operators", () => {
expect(parseCommand('npm install && npm test\ngit add .\ngit commit -m "test"')).toEqual([
"npm install",
"npm test",
"git add .",
'git commit -m "test"',
])
})

it("splits on actual newlines even within quotes", () => {
// Note: Since we split by newlines first, actual newlines in the input
// will split the command, even if they appear to be within quotes
// Using template literal to create actual newline
const commandWithNewlineInQuotes = `echo "Hello
World"
git status`
// The quotes get stripped because they're no longer properly paired after splitting
expect(parseCommand(commandWithNewlineInQuotes)).toEqual(["echo Hello", "World", "git status"])
})

it("handles quoted strings on single line", () => {
// When quotes are on the same line, they are preserved
expect(parseCommand('echo "Hello World"\ngit status')).toEqual(['echo "Hello World"', "git status"])
})

it("handles complex multi-line commands", () => {
const multiLineCommand = `npm install
npm test && npm run build
echo "Done" | tee output.log
git status; git add .
ls -la || echo "Failed"`

expect(parseCommand(multiLineCommand)).toEqual([
"npm install",
"npm test",
"npm run build",
'echo "Done"',
"tee output.log",
"git status",
"git add .",
"ls -la",
'echo "Failed"',
])
})

it("handles newlines with subshells", () => {
expect(parseCommand("echo $(date)\nnpm test\ngit status")).toEqual([
"echo",
"date",
"npm test",
"git status",
])
})

it("handles newlines with redirections", () => {
expect(parseCommand("npm test 2>&1\necho done\nls -la > files.txt")).toEqual([
"npm test 2>&1",
"echo done",
"ls -la > files.txt",
])
})

it("handles empty input with newlines", () => {
expect(parseCommand("\n\n\n")).toEqual([])
expect(parseCommand("\r\n\r\n")).toEqual([])
expect(parseCommand("\r\r\r")).toEqual([])
})

it("handles whitespace-only lines", () => {
expect(parseCommand("echo hello\n \t \ngit status")).toEqual(["echo hello", "git status"])
})
})
})

describe("isAutoApprovedSingleCommand (legacy behavior)", () => {
Expand Down
26 changes: 25 additions & 1 deletion webview-ui/src/utils/command-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,41 @@ type ShellToken = string | { op: string } | { command: string }

/**
* Split a command string into individual sub-commands by
* chaining operators (&&, ||, ;, or |).
* chaining operators (&&, ||, ;, or |) and newlines.
*
* Uses shell-quote to properly handle:
* - Quoted strings (preserves quotes)
* - Subshell commands ($(cmd) or `cmd`)
* - PowerShell redirections (2>&1)
* - Chain operators (&&, ||, ;, |)
* - Newlines as command separators
*/
export function parseCommand(command: string): string[] {
if (!command?.trim()) return []

// Split by newlines first (handle different line ending formats)
// This regex splits on \r\n (Windows), \n (Unix), or \r (old Mac)
const lines = command.split(/\r\n|\r|\n/)
const allCommands: string[] = []

for (const line of lines) {
// Skip empty lines
if (!line.trim()) continue

// Process each line through the existing parsing logic
const lineCommands = parseCommandLine(line)
allCommands.push(...lineCommands)
}

return allCommands
}

/**
* Parse a single line of commands (internal helper function)
*/
function parseCommandLine(command: string): string[] {
if (!command?.trim()) return []

// Storage for replaced content
const redirections: string[] = []
const subshells: string[] = []
Expand Down