Skip to content

Commit 1573edf

Browse files
roomote-v0[bot]roomotehannesrudolph
authored
fix: make command chaining examples shell-aware for Windows compatibility (#10434)
* fix: make command chaining examples shell-aware for Windows compatibility Addresses Issue #10352 where Roo Code generates Unix-style command chaining (&&) even on Windows systems using PowerShell or cmd.exe. Changes: - Add getCommandChainOperator() to detect the user shell and return the appropriate command chaining syntax: - Unix shells (bash, zsh, etc.): && - PowerShell: ; - cmd.exe: & - Update getRulesSection() to use shell-specific chaining in examples - Add informative note for non-Unix shells about different syntaxes - Add comprehensive tests for shell detection and command chaining * feat: add Unix utility guidance for Windows shells Addresses feedback from issue #10352 about sed and other Unix-specific utilities being suggested on Windows. The system prompt now includes guidance for PowerShell and cmd.exe users to use native alternatives: PowerShell: - Select-String instead of grep - Get-Content instead of cat - Remove-Item instead of rm - Copy-Item instead of cp - Move-Item instead of mv - -replace operator or [regex] instead of sed cmd.exe: - type instead of cat - del instead of rm - copy instead of cp - move instead of mv - find/findstr instead of grep * Apply suggestion from @roomote[bot] Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> * fix: use && for cmd.exe to preserve conditional execution semantics - Update getCommandChainOperator() to return && for cmd.exe (already done) - Update getCommandChainNote() to document && instead of & for cmd.exe - Update JSDoc to reflect cmd.exe uses && for conditional execution - Update tests to expect && for cmd.exe cmd.exe supports && for conditional execution (run next command only if previous succeeds), which provides the same semantics as Unix shells. * fix: update PowerShell note to use && for cmd.exe reference --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Hannes Rudolph <hrudolph@gmail.com> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
1 parent ca0c901 commit 1573edf

2 files changed

Lines changed: 168 additions & 2 deletions

File tree

src/core/prompts/__tests__/sections.spec.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { addCustomInstructions } from "../sections/custom-instructions"
22
import { getCapabilitiesSection } from "../sections/capabilities"
3-
import { getRulesSection } from "../sections/rules"
3+
import { getRulesSection, getCommandChainOperator } from "../sections/rules"
44
import { McpHub } from "../../../services/mcp/McpHub"
5+
import * as shellUtils from "../../../utils/shell"
56

67
describe("addCustomInstructions", () => {
78
it("adds vscode language to custom instructions", async () => {
@@ -114,3 +115,117 @@ describe("getRulesSection", () => {
114115
expect(result).not.toContain("Never reveal the vendor or company")
115116
})
116117
})
118+
119+
describe("getCommandChainOperator", () => {
120+
it("returns && for bash shell", () => {
121+
vi.spyOn(shellUtils, "getShell").mockReturnValue("/bin/bash")
122+
expect(getCommandChainOperator()).toBe("&&")
123+
})
124+
125+
it("returns && for zsh shell", () => {
126+
vi.spyOn(shellUtils, "getShell").mockReturnValue("/bin/zsh")
127+
expect(getCommandChainOperator()).toBe("&&")
128+
})
129+
130+
it("returns ; for PowerShell", () => {
131+
vi.spyOn(shellUtils, "getShell").mockReturnValue(
132+
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
133+
)
134+
expect(getCommandChainOperator()).toBe(";")
135+
})
136+
137+
it("returns ; for PowerShell Core (pwsh)", () => {
138+
vi.spyOn(shellUtils, "getShell").mockReturnValue("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
139+
expect(getCommandChainOperator()).toBe(";")
140+
})
141+
142+
it("returns && for cmd.exe", () => {
143+
vi.spyOn(shellUtils, "getShell").mockReturnValue("C:\\Windows\\System32\\cmd.exe")
144+
expect(getCommandChainOperator()).toBe("&&")
145+
})
146+
147+
it("returns && for Git Bash on Windows", () => {
148+
vi.spyOn(shellUtils, "getShell").mockReturnValue("C:\\Program Files\\Git\\bin\\bash.exe")
149+
expect(getCommandChainOperator()).toBe("&&")
150+
})
151+
152+
it("returns && for WSL bash", () => {
153+
vi.spyOn(shellUtils, "getShell").mockReturnValue("/bin/bash")
154+
expect(getCommandChainOperator()).toBe("&&")
155+
})
156+
})
157+
158+
describe("getRulesSection shell-aware command chaining", () => {
159+
const cwd = "/test/path"
160+
161+
afterEach(() => {
162+
vi.restoreAllMocks()
163+
})
164+
165+
it("uses && for Unix shells in command chaining example", () => {
166+
vi.spyOn(shellUtils, "getShell").mockReturnValue("/bin/bash")
167+
const result = getRulesSection(cwd)
168+
169+
expect(result).toContain("cd (path to project) && (command")
170+
expect(result).not.toContain("cd (path to project) ; (command")
171+
expect(result).not.toContain("cd (path to project) & (command")
172+
})
173+
174+
it("uses ; for PowerShell in command chaining example", () => {
175+
vi.spyOn(shellUtils, "getShell").mockReturnValue(
176+
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
177+
)
178+
const result = getRulesSection(cwd)
179+
180+
expect(result).toContain("cd (path to project) ; (command")
181+
expect(result).toContain("Note: Using `;` for PowerShell command chaining")
182+
})
183+
184+
it("uses && for cmd.exe in command chaining example", () => {
185+
vi.spyOn(shellUtils, "getShell").mockReturnValue("C:\\Windows\\System32\\cmd.exe")
186+
const result = getRulesSection(cwd)
187+
188+
expect(result).toContain("cd (path to project) && (command")
189+
expect(result).toContain("Note: Using `&&` for cmd.exe command chaining")
190+
})
191+
192+
it("includes Unix utility guidance for PowerShell", () => {
193+
vi.spyOn(shellUtils, "getShell").mockReturnValue(
194+
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
195+
)
196+
const result = getRulesSection(cwd)
197+
198+
expect(result).toContain("IMPORTANT: When using PowerShell, avoid Unix-specific utilities")
199+
expect(result).toContain("`sed`, `grep`, `awk`, `cat`, `rm`, `cp`, `mv`")
200+
expect(result).toContain("`Select-String` for grep")
201+
expect(result).toContain("`Get-Content` for cat")
202+
expect(result).toContain("PowerShell's `-replace` operator")
203+
})
204+
205+
it("includes Unix utility guidance for cmd.exe", () => {
206+
vi.spyOn(shellUtils, "getShell").mockReturnValue("C:\\Windows\\System32\\cmd.exe")
207+
const result = getRulesSection(cwd)
208+
209+
expect(result).toContain("IMPORTANT: When using cmd.exe, avoid Unix-specific utilities")
210+
expect(result).toContain("`sed`, `grep`, `awk`, `cat`, `rm`, `cp`, `mv`")
211+
expect(result).toContain("`type` for cat")
212+
expect(result).toContain("`del` for rm")
213+
expect(result).toContain("`find`/`findstr` for grep")
214+
})
215+
216+
it("does not include Unix utility guidance for Unix shells", () => {
217+
vi.spyOn(shellUtils, "getShell").mockReturnValue("/bin/bash")
218+
const result = getRulesSection(cwd)
219+
220+
expect(result).not.toContain("IMPORTANT: When using PowerShell")
221+
expect(result).not.toContain("IMPORTANT: When using cmd.exe")
222+
expect(result).not.toContain("`Select-String` for grep")
223+
})
224+
225+
it("does not include note for Unix shells", () => {
226+
vi.spyOn(shellUtils, "getShell").mockReturnValue("/bin/zsh")
227+
const result = getRulesSection(cwd)
228+
229+
expect(result).not.toContain("Note: Using")
230+
})
231+
})

src/core/prompts/sections/rules.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,53 @@
11
import type { SystemPromptSettings } from "../types"
22
import { getEffectiveProtocol, isNativeProtocol } from "@roo-code/types"
33

4+
import { getShell } from "../../../utils/shell"
5+
6+
/**
7+
* Returns the appropriate command chaining operator based on the user's shell.
8+
* - Unix shells (bash, zsh, etc.): `&&` (run next command only if previous succeeds)
9+
* - PowerShell: `;` (semicolon for command separation)
10+
* - cmd.exe: `&&` (conditional execution, same as Unix)
11+
* @internal Exported for testing purposes
12+
*/
13+
export function getCommandChainOperator(): string {
14+
const shell = getShell().toLowerCase()
15+
16+
// Check for PowerShell (both Windows PowerShell and PowerShell Core)
17+
if (shell.includes("powershell") || shell.includes("pwsh")) {
18+
return ";"
19+
}
20+
21+
// Check for cmd.exe
22+
if (shell.includes("cmd.exe")) {
23+
return "&&"
24+
}
25+
26+
// Default to Unix-style && for bash, zsh, sh, and other shells
27+
// This also covers Git Bash, WSL, and other Unix-like environments on Windows
28+
return "&&"
29+
}
30+
31+
/**
32+
* Returns a shell-specific note about command chaining syntax and platform-specific utilities.
33+
*/
34+
function getCommandChainNote(): string {
35+
const shell = getShell().toLowerCase()
36+
37+
// Check for PowerShell
38+
if (shell.includes("powershell") || shell.includes("pwsh")) {
39+
return "Note: Using `;` for PowerShell command chaining. For bash/zsh use `&&`, for cmd.exe use `&&`. IMPORTANT: When using PowerShell, avoid Unix-specific utilities like `sed`, `grep`, `awk`, `cat`, `rm`, `cp`, `mv`. Instead use PowerShell equivalents: `Select-String` for grep, `Get-Content` for cat, `Remove-Item` for rm, `Copy-Item` for cp, `Move-Item` for mv, and PowerShell's `-replace` operator or `[regex]` for sed."
40+
}
41+
42+
// Check for cmd.exe
43+
if (shell.includes("cmd.exe")) {
44+
return "Note: Using `&&` for cmd.exe command chaining (conditional execution). For bash/zsh use `&&`, for PowerShell use `;`. IMPORTANT: When using cmd.exe, avoid Unix-specific utilities like `sed`, `grep`, `awk`, `cat`, `rm`, `cp`, `mv`. Use built-in commands like `type` for cat, `del` for rm, `copy` for cp, `move` for mv, `find`/`findstr` for grep, or consider using PowerShell commands instead."
45+
}
46+
47+
// Unix shells
48+
return ""
49+
}
50+
451
function getVendorConfidentialitySection(): string {
552
return `
653
@@ -20,6 +67,10 @@ export function getRulesSection(cwd: string, settings?: SystemPromptSettings): s
2067
// Determine whether to use XML tool references based on protocol
2168
const effectiveProtocol = getEffectiveProtocol(settings?.toolProtocol)
2269

70+
// Get shell-appropriate command chaining operator
71+
const chainOp = getCommandChainOperator()
72+
const chainNote = getCommandChainNote()
73+
2374
return `====
2475
2576
RULES
@@ -28,7 +79,7 @@ RULES
2879
- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to ${isNativeProtocol(effectiveProtocol) ? "execute_command" : "<execute_command>"}.
2980
- You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '${cwd.toPosix()}', so be sure to pass in the correct 'path' parameter when using tools that require a path.
3081
- Do not use the ~ character or $HOME to refer to the home directory.
31-
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`.
82+
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory ${chainOp} then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) ${chainOp} (command, in this case npm install)\`.${chainNote ? ` ${chainNote}` : ""}
3283
- Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode.
3384
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
3485
* For example, in architect mode trying to edit app.js would be rejected because architect mode can only edit files matching "\\.md$"

0 commit comments

Comments
 (0)