Skip to content

Commit 5af3511

Browse files
majiayu000claude
andauthored
fix: handle Windows CRLF line endings in grep tool (anomalyco#5948)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent eab177f commit 5af3511

3 files changed

Lines changed: 114 additions & 3 deletions

File tree

packages/opencode/src/file/ripgrep.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ export namespace Ripgrep {
240240
if (done) break
241241

242242
buffer += decoder.decode(value, { stream: true })
243-
const lines = buffer.split("\n")
243+
// Handle both Unix (\n) and Windows (\r\n) line endings
244+
const lines = buffer.split(/\r?\n/)
244245
buffer = lines.pop() || ""
245246

246247
for (const line of lines) {
@@ -379,7 +380,8 @@ export namespace Ripgrep {
379380
return []
380381
}
381382

382-
const lines = result.text().trim().split("\n").filter(Boolean)
383+
// Handle both Unix (\n) and Windows (\r\n) line endings
384+
const lines = result.text().trim().split(/\r?\n/).filter(Boolean)
383385
// Parse JSON lines from ripgrep output
384386

385387
return lines

packages/opencode/src/tool/grep.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export const GrepTool = Tool.define("grep", {
4949
throw new Error(`ripgrep failed: ${errorOutput}`)
5050
}
5151

52-
const lines = output.trim().split("\n")
52+
// Handle both Unix (\n) and Windows (\r\n) line endings
53+
const lines = output.trim().split(/\r?\n/)
5354
const matches = []
5455

5556
for (const line of lines) {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, test } from "bun:test"
2+
import path from "path"
3+
import { GrepTool } from "../../src/tool/grep"
4+
import { Instance } from "../../src/project/instance"
5+
import { tmpdir } from "../fixture/fixture"
6+
7+
const ctx = {
8+
sessionID: "test",
9+
messageID: "",
10+
callID: "",
11+
agent: "build",
12+
abort: AbortSignal.any([]),
13+
metadata: () => {},
14+
}
15+
16+
const projectRoot = path.join(__dirname, "../..")
17+
18+
describe("tool.grep", () => {
19+
test("basic search", async () => {
20+
await Instance.provide({
21+
directory: projectRoot,
22+
fn: async () => {
23+
const grep = await GrepTool.init()
24+
const result = await grep.execute(
25+
{
26+
pattern: "export",
27+
path: path.join(projectRoot, "src/tool"),
28+
include: "*.ts",
29+
},
30+
ctx,
31+
)
32+
expect(result.metadata.matches).toBeGreaterThan(0)
33+
expect(result.output).toContain("Found")
34+
},
35+
})
36+
})
37+
38+
test("no matches returns correct output", async () => {
39+
await using tmp = await tmpdir({
40+
init: async (dir) => {
41+
await Bun.write(path.join(dir, "test.txt"), "hello world")
42+
},
43+
})
44+
await Instance.provide({
45+
directory: tmp.path,
46+
fn: async () => {
47+
const grep = await GrepTool.init()
48+
const result = await grep.execute(
49+
{
50+
pattern: "xyznonexistentpatternxyz123",
51+
path: tmp.path,
52+
},
53+
ctx,
54+
)
55+
expect(result.metadata.matches).toBe(0)
56+
expect(result.output).toBe("No files found")
57+
},
58+
})
59+
})
60+
61+
test("handles CRLF line endings in output", async () => {
62+
// This test verifies the regex split handles both \n and \r\n
63+
await using tmp = await tmpdir({
64+
init: async (dir) => {
65+
// Create a test file with content
66+
await Bun.write(path.join(dir, "test.txt"), "line1\nline2\nline3")
67+
},
68+
})
69+
await Instance.provide({
70+
directory: tmp.path,
71+
fn: async () => {
72+
const grep = await GrepTool.init()
73+
const result = await grep.execute(
74+
{
75+
pattern: "line",
76+
path: tmp.path,
77+
},
78+
ctx,
79+
)
80+
expect(result.metadata.matches).toBeGreaterThan(0)
81+
},
82+
})
83+
})
84+
})
85+
86+
describe("CRLF regex handling", () => {
87+
test("regex correctly splits Unix line endings", () => {
88+
const unixOutput = "file1.txt|1|content1\nfile2.txt|2|content2\nfile3.txt|3|content3"
89+
const lines = unixOutput.trim().split(/\r?\n/)
90+
expect(lines.length).toBe(3)
91+
expect(lines[0]).toBe("file1.txt|1|content1")
92+
expect(lines[2]).toBe("file3.txt|3|content3")
93+
})
94+
95+
test("regex correctly splits Windows CRLF line endings", () => {
96+
const windowsOutput = "file1.txt|1|content1\r\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
97+
const lines = windowsOutput.trim().split(/\r?\n/)
98+
expect(lines.length).toBe(3)
99+
expect(lines[0]).toBe("file1.txt|1|content1")
100+
expect(lines[2]).toBe("file3.txt|3|content3")
101+
})
102+
103+
test("regex handles mixed line endings", () => {
104+
const mixedOutput = "file1.txt|1|content1\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
105+
const lines = mixedOutput.trim().split(/\r?\n/)
106+
expect(lines.length).toBe(3)
107+
})
108+
})

0 commit comments

Comments
 (0)