From 453e0677d63a5cf16cc77006caae29ccf37e4d77 Mon Sep 17 00:00:00 2001 From: Himess Date: Fri, 6 Mar 2026 15:54:33 +0300 Subject: [PATCH] fix: use regex for search output parsing to handle Windows drive-letter paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ripgrep/grep output parser uses `split(':', 2)` to extract file:lineno:content from match lines. On Windows, absolute paths contain a drive letter colon (e.g. `C:\Users\foo\bar.py:42:content`), so `split(':', 2)` produces `["C", "\Users\...", "42:content"]`. `int(parts[1])` then raises ValueError and the match is silently dropped. All search results are lost on Windows. Same category as #390 — string-based path parsing that fails on Windows. Replace `split()` with a regex that optionally captures the drive letter prefix: `^([A-Za-z]:)?(.*?):(\d+):(.*)$`. Applied to both `_search_with_rg` and `_search_with_grep`. --- tools/file_operations.py | 81 +++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/tools/file_operations.py b/tools/file_operations.py index 182d35f5f..a876ab867 100644 --- a/tools/file_operations.py +++ b/tools/file_operations.py @@ -943,37 +943,35 @@ class ShellFileOperations(FileOperations): # rg match lines: "file:lineno:content" (colon separator) # rg context lines: "file-lineno-content" (dash separator) # rg group seps: "--" + # Note: on Windows, paths contain drive letters (e.g. C:\path), + # so naive split(":") breaks. Use regex to handle both platforms. + _match_re = re.compile(r'^([A-Za-z]:)?(.*?):(\d+):(.*)$') + _ctx_re = re.compile(r'^([A-Za-z]:)?(.*?)-(\d+)-(.*)$') matches = [] for line in result.stdout.strip().split('\n'): if not line or line == "--": continue # Try match line first (colon-separated: file:line:content) - parts = line.split(':', 2) - if len(parts) >= 3: - try: - matches.append(SearchMatch( - path=parts[0], - line_number=int(parts[1]), - content=parts[2][:500] - )) - continue - except ValueError: - pass + m = _match_re.match(line) + if m: + matches.append(SearchMatch( + path=(m.group(1) or '') + m.group(2), + line_number=int(m.group(3)), + content=m.group(4)[:500] + )) + continue # Try context line (dash-separated: file-line-content) # Only attempt if context was requested to avoid false positives if context > 0: - parts = line.split('-', 2) - if len(parts) >= 3: - try: - matches.append(SearchMatch( - path=parts[0], - line_number=int(parts[1]), - content=parts[2][:500] - )) - except ValueError: - pass + m = _ctx_re.match(line) + if m: + matches.append(SearchMatch( + path=(m.group(1) or '') + m.group(2), + line_number=int(m.group(3)), + content=m.group(4)[:500] + )) total = len(matches) page = matches[offset:offset + limit] @@ -1035,34 +1033,33 @@ class ShellFileOperations(FileOperations): # grep match lines: "file:lineno:content" (colon) # grep context lines: "file-lineno-content" (dash) # grep group seps: "--" + # Note: on Windows, paths contain drive letters (e.g. C:\path), + # so naive split(":") breaks. Use regex to handle both platforms. + _match_re = re.compile(r'^([A-Za-z]:)?(.*?):(\d+):(.*)$') + _ctx_re = re.compile(r'^([A-Za-z]:)?(.*?)-(\d+)-(.*)$') matches = [] for line in result.stdout.strip().split('\n'): if not line or line == "--": continue - parts = line.split(':', 2) - if len(parts) >= 3: - try: - matches.append(SearchMatch( - path=parts[0], - line_number=int(parts[1]), - content=parts[2][:500] - )) - continue - except ValueError: - pass + m = _match_re.match(line) + if m: + matches.append(SearchMatch( + path=(m.group(1) or '') + m.group(2), + line_number=int(m.group(3)), + content=m.group(4)[:500] + )) + continue if context > 0: - parts = line.split('-', 2) - if len(parts) >= 3: - try: - matches.append(SearchMatch( - path=parts[0], - line_number=int(parts[1]), - content=parts[2][:500] - )) - except ValueError: - pass + m = _ctx_re.match(line) + if m: + matches.append(SearchMatch( + path=(m.group(1) or '') + m.group(2), + line_number=int(m.group(3)), + content=m.group(4)[:500] + )) + total = len(matches) page = matches[offset:offset + limit]