From e1d09cddb5d810198e09591a794349428738ae25 Mon Sep 17 00:00:00 2001 From: yangxianzhao <1430552949@qq.com> Date: Fri, 30 Oct 2026 14:29:16 +0800 Subject: [PATCH] fix(core): keep literal backslashes in Unix file names normalizePath replaced every backslash with a slash on all platforms, so glob and grep reported a nonexistent path for files whose name contains a backslash on Unix. Only Windows needs separator normalization. --- packages/core/src/ripgrep.ts | 8 ++++---- packages/core/test/ripgrep.test.ts | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 5e49d46223ca..0a4f8c5c83a7 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -91,11 +91,11 @@ export class Service extends Context.Service()("@opencode/Ri const failure = (message: string, cause?: unknown) => new Error({ message, cause }) +// A backslash is a literal character in Unix file names; only Windows reports separators that way. const normalizePath = (value: string) => - value - .replace(/^(?:\.[\\/])+/u, "") - .replace(/^[\\/]+/u, "") - .replaceAll("\\", "/") + process.platform === "win32" + ? value.replace(/^(?:\.[\\/])+/u, "").replace(/^[\\/]+/u, "").replaceAll("\\", "/") + : value.replace(/^(?:\.\/)+/u, "").replace(/^\/+/u, "") const isInvalidPattern = (stderr: string) => stderr.includes("regex parse error") || stderr.includes("error parsing regex") diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index bf4231ec7e2f..e36d25482c36 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -186,6 +186,27 @@ describe("Ripgrep", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ), ) + + const posixOnly = process.platform === "win32" ? it.live.skip : it.live + + posixOnly("keeps literal backslashes in Unix file names", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + const file = "actual\\name.txt" + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, file), "needle\n")) + + const ripgrep = yield* Ripgrep.Service + const files = yield* ripgrep.glob({ cwd: tmp.path, pattern: "*.txt", limit: 10 }) + expect(files.map((item) => item.path)).toEqual([RelativePath.make(file)]) + + const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", limit: 10 }) + expect(matches.map((item) => item.entry.path)).toEqual([RelativePath.make(file)]) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) }) describe("RipgrepBinary.verify", () => {