Skip to content

Commit 9d434c2

Browse files
authored
Split commands on newlines (RooCodeInc#6121)
1 parent 714fafd commit 9d434c2

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed

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

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,121 @@ describe("Command Validation", () => {
5050
parseCommand('npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"'),
5151
).toEqual(["npm test", 'Select-String -NotMatch "node_modules"', 'Select-String "FAIL|Error"'])
5252
})
53+
54+
describe("newline handling", () => {
55+
it("splits commands by Unix newlines (\\n)", () => {
56+
expect(parseCommand("echo hello\ngit status\nnpm install")).toEqual([
57+
"echo hello",
58+
"git status",
59+
"npm install",
60+
])
61+
})
62+
63+
it("splits commands by Windows newlines (\\r\\n)", () => {
64+
expect(parseCommand("echo hello\r\ngit status\r\nnpm install")).toEqual([
65+
"echo hello",
66+
"git status",
67+
"npm install",
68+
])
69+
})
70+
71+
it("splits commands by old Mac newlines (\\r)", () => {
72+
expect(parseCommand("echo hello\rgit status\rnpm install")).toEqual([
73+
"echo hello",
74+
"git status",
75+
"npm install",
76+
])
77+
})
78+
79+
it("handles mixed line endings", () => {
80+
expect(parseCommand("echo hello\ngit status\r\nnpm install\rls -la")).toEqual([
81+
"echo hello",
82+
"git status",
83+
"npm install",
84+
"ls -la",
85+
])
86+
})
87+
88+
it("ignores empty lines", () => {
89+
expect(parseCommand("echo hello\n\n\ngit status\r\n\r\nnpm install")).toEqual([
90+
"echo hello",
91+
"git status",
92+
"npm install",
93+
])
94+
})
95+
96+
it("handles newlines with chain operators", () => {
97+
expect(parseCommand('npm install && npm test\ngit add .\ngit commit -m "test"')).toEqual([
98+
"npm install",
99+
"npm test",
100+
"git add .",
101+
'git commit -m "test"',
102+
])
103+
})
104+
105+
it("splits on actual newlines even within quotes", () => {
106+
// Note: Since we split by newlines first, actual newlines in the input
107+
// will split the command, even if they appear to be within quotes
108+
// Using template literal to create actual newline
109+
const commandWithNewlineInQuotes = `echo "Hello
110+
World"
111+
git status`
112+
// The quotes get stripped because they're no longer properly paired after splitting
113+
expect(parseCommand(commandWithNewlineInQuotes)).toEqual(["echo Hello", "World", "git status"])
114+
})
115+
116+
it("handles quoted strings on single line", () => {
117+
// When quotes are on the same line, they are preserved
118+
expect(parseCommand('echo "Hello World"\ngit status')).toEqual(['echo "Hello World"', "git status"])
119+
})
120+
121+
it("handles complex multi-line commands", () => {
122+
const multiLineCommand = `npm install
123+
npm test && npm run build
124+
echo "Done" | tee output.log
125+
git status; git add .
126+
ls -la || echo "Failed"`
127+
128+
expect(parseCommand(multiLineCommand)).toEqual([
129+
"npm install",
130+
"npm test",
131+
"npm run build",
132+
'echo "Done"',
133+
"tee output.log",
134+
"git status",
135+
"git add .",
136+
"ls -la",
137+
'echo "Failed"',
138+
])
139+
})
140+
141+
it("handles newlines with subshells", () => {
142+
expect(parseCommand("echo $(date)\nnpm test\ngit status")).toEqual([
143+
"echo",
144+
"date",
145+
"npm test",
146+
"git status",
147+
])
148+
})
149+
150+
it("handles newlines with redirections", () => {
151+
expect(parseCommand("npm test 2>&1\necho done\nls -la > files.txt")).toEqual([
152+
"npm test 2>&1",
153+
"echo done",
154+
"ls -la > files.txt",
155+
])
156+
})
157+
158+
it("handles empty input with newlines", () => {
159+
expect(parseCommand("\n\n\n")).toEqual([])
160+
expect(parseCommand("\r\n\r\n")).toEqual([])
161+
expect(parseCommand("\r\r\r")).toEqual([])
162+
})
163+
164+
it("handles whitespace-only lines", () => {
165+
expect(parseCommand("echo hello\n \t \ngit status")).toEqual(["echo hello", "git status"])
166+
})
167+
})
53168
})
54169

55170
describe("isAutoApprovedSingleCommand (legacy behavior)", () => {

webview-ui/src/utils/command-validation.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,41 @@ type ShellToken = string | { op: string } | { command: string }
6060

6161
/**
6262
* Split a command string into individual sub-commands by
63-
* chaining operators (&&, ||, ;, or |).
63+
* chaining operators (&&, ||, ;, or |) and newlines.
6464
*
6565
* Uses shell-quote to properly handle:
6666
* - Quoted strings (preserves quotes)
6767
* - Subshell commands ($(cmd) or `cmd`)
6868
* - PowerShell redirections (2>&1)
6969
* - Chain operators (&&, ||, ;, |)
70+
* - Newlines as command separators
7071
*/
7172
export function parseCommand(command: string): string[] {
7273
if (!command?.trim()) return []
7374

75+
// Split by newlines first (handle different line ending formats)
76+
// This regex splits on \r\n (Windows), \n (Unix), or \r (old Mac)
77+
const lines = command.split(/\r\n|\r|\n/)
78+
const allCommands: string[] = []
79+
80+
for (const line of lines) {
81+
// Skip empty lines
82+
if (!line.trim()) continue
83+
84+
// Process each line through the existing parsing logic
85+
const lineCommands = parseCommandLine(line)
86+
allCommands.push(...lineCommands)
87+
}
88+
89+
return allCommands
90+
}
91+
92+
/**
93+
* Parse a single line of commands (internal helper function)
94+
*/
95+
function parseCommandLine(command: string): string[] {
96+
if (!command?.trim()) return []
97+
7498
// Storage for replaced content
7599
const redirections: string[] = []
76100
const subshells: string[] = []

0 commit comments

Comments
 (0)