Split commands on newlines (#6121)

This commit is contained in:
Matt Rubens 2025-07-23 11:21:06 -04:00 committed by GitHub
parent 714fafd328
commit 9d434c2db9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 140 additions and 1 deletions

View file

@ -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)", () => {

View file

@ -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[] = []