From 35a24bce0a9f1b214b0f29bb23f170b437f5a132 Mon Sep 17 00:00:00 2001 From: Harsh Jha Date: Fri, 24 Jul 2026 01:36:29 +0530 Subject: [PATCH] Fix ripgrep output parsing on Windows drive-letter paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _parse_rg split each line on the first two colons, but ripgrep is invoked with an absolute path — on Windows that starts with a drive letter (C:\...) whose colon was mistaken for the path:line:text separator, so every match reported file "C", line 0, with the line number leaked into the text. Anchor the parse on the numeric :: field with a regex so the drive-letter colon and any colons in the matched text are preserved. Co-Authored-By: Claude Opus 4.8 (1M context) --- coworker/tools/search.py | 16 ++++++++++++---- tests/test_code_tools.py | 19 ++++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/coworker/tools/search.py b/coworker/tools/search.py index ad489e1fa2..bb03c0fb75 100644 --- a/coworker/tools/search.py +++ b/coworker/tools/search.py @@ -134,16 +134,24 @@ def _rel(path: str, root: Path) -> str: return path +# ripgrep --no-heading --line-number emits "::". A positional +# split(":", 2) mis-parses on Windows, where the absolute path starts with a +# drive letter ("C:\...") whose colon looks like the field separator. Anchor on +# the numeric "::" instead, so both the drive-letter colon and any colons +# inside the matched text stay intact. +_RG_LINE = re.compile(r"(.+?):(\d+):(.*)") + + def _parse_rg(stdout: str, root: Path, n: int) -> dict[str, Any]: matches: list[dict[str, Any]] = [] for line in stdout.splitlines(): - parts = line.split(":", 2) - if len(parts) == 3: - f, ln, txt = parts + m = _RG_LINE.match(line) + if m: + f, ln, txt = m.group(1), m.group(2), m.group(3) matches.append( { "file": _rel(f, root), - "line": int(ln) if ln.isdigit() else 0, + "line": int(ln), "text": txt[:300], } ) diff --git a/tests/test_code_tools.py b/tests/test_code_tools.py index 0edc83b2f1..5e78128135 100644 --- a/tests/test_code_tools.py +++ b/tests/test_code_tools.py @@ -13,7 +13,7 @@ from coworker.tools.files import file_tools from coworker.tools.git import git_tools -from coworker.tools.search import _py_grep, search_tools +from coworker.tools.search import _parse_rg, _py_grep, search_tools from coworker.web.fetch import _html_to_text, make_web_fetch_tool @@ -61,6 +61,23 @@ def test_ripgrep_uses_the_same_ignored_dirs_as_the_python_fallback(tmp_path, mon assert commands[0].index(f"!**/{ignored}/**") > user_glob +def test_parse_rg_handles_windows_drive_letter_and_colons_in_text(): + # ripgrep prints absolute paths; on Windows they begin with a drive letter + # ("C:\\..."), whose colon must not be mistaken for the path:line:text + # separator. The matched text can also contain colons. Hardcode a Windows + # line so this guards the parser on every platform, not just Windows CI. + from pathlib import Path + + stdout = "C:\\Users\\me\\ws\\a.py:12:x = {'k': 'v'}\n" + out = _parse_rg(stdout, Path("C:\\Users\\me\\ws"), 100) + + assert out["count"] == 1 + m = out["matches"][0] + assert m["line"] == 12 # was 0 with the old split(":", 2) + assert m["text"] == "x = {'k': 'v'}" # line number not leaked, colons kept + assert m["file"] != "C" # the drive letter is not the filename + + def test_grep_rejects_path_escape(tmp_path): grep = search_tools(str(tmp_path))[0] assert "escapes" in grep(pattern="x", path="../..")["error"]