Sfoglia il codice sorgente

Split commands on newlines (#6121)

Matt Rubens 7 mesi fa
parent
commit
9d434c2db9

+ 115 - 0
webview-ui/src/utils/__tests__/command-validation.spec.ts

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

+ 25 - 1
webview-ui/src/utils/command-validation.ts

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