#!/usr/bin/env python3 # ollie:parallel read # args_json: {"type":"object","required":["pattern"],"properties":{"pattern":{"type":"string","description":"Regex pattern to search"},"path":{"type":"string","description":"Search directory (default: cwd)"},"mode":{"type":"string","description":"content|files_with_matches|count (default: content)"},"glob":{"type":"string","description":"File glob filter"},"type":{"type":"string","description":"File type filter (rg --type)"},"case_insensitive":{"type":"boolean","description":"Case-insensitive search"},"after":{"type":"integer","description":"Lines after match (-A)"},"before":{"type":"integer","description":"Lines before match (-B)"},"context":{"type":"integer","description":"Lines around match (-C)"},"multiline":{"type":"boolean","description":"Enable multiline matching"},"head":{"type":"integer","description":"Max results to return (default: 50)"},"offset":{"type":"integer","description":"Skip first N results"}}} # ollie:prompt # ## file_grep # # Search file contents with ripgrep. Capped at 50 matches by default. # # **Args**: `pattern` (required), plus optional named params. # # ``` # file_grep(pattern="pattern", path="/abs/dir") # file_grep(pattern="TODO", path="src/", glob="*.go", case_insensitive=true) # file_grep(pattern="func main", path="/home/user/project") # ``` # # - `path`: search directory (not a positional arg) # - `glob`: file glob filter # - `case_insensitive`: boolean # # **Params**: `path`, `mode` (content|files_with_matches|count), `glob`, `type`, `case_insensitive`, `before`, `after`, `context`, `multiline`, `head`, `offset`. # ollie:end import sys import os import subprocess import shutil sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args args = parse_args() pattern = args.require("pattern") search_path = args.get("path", os.getcwd()) mode = args.get("mode", "content") glob_pat = args.get("glob") type_filter = args.get("type") case_insensitive = args.get_bool("case_insensitive") after = args.get_int("after") before = args.get_int("before") context_lines = args.get_int("context") multiline = args.get_bool("multiline") head = args.get_int("head", 0) offset = args.get_int("offset", 0) use_rg = shutil.which('rg') is not None if use_rg: cmd = ['rg', '--no-config', '--color=never'] if mode == 'files_with_matches': cmd.append('-l') elif mode == 'count': cmd.append('--count') else: cmd.append('-n') if case_insensitive: cmd.append('-i') if multiline: cmd += ['-U', '--multiline-dotall'] if glob_pat: cmd += ['-g', glob_pat] if type_filter: cmd += ['-t', type_filter] if context_lines is not None: cmd += ['-C', str(context_lines)] if after is not None: cmd += ['-A', str(after)] if before is not None: cmd += ['-B', str(before)] cmd += ['-e', pattern, search_path] else: cmd = ['grep', '-r', '--color=never'] if mode == 'files_with_matches': cmd.append('-l') elif mode == 'count': cmd.append('-c') else: cmd.append('-n') if case_insensitive: cmd.append('-i') if glob_pat: cmd += ['--include=' + glob_pat] if context_lines is not None: cmd += ['-C', str(context_lines)] if after is not None: cmd += ['-A', str(after)] if before is not None: cmd += ['-B', str(before)] cmd += ['-e', pattern, search_path] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 1: print("STATUS=no_matches MATCH_COUNT=0") print("no matches") sys.exit(0) if result.returncode > 1: print(f"STATUS=error") print(f"error: {result.stderr.strip() or f'search exited with {result.returncode}'}") sys.exit(1) lines = result.stdout.splitlines() total = len(lines) if offset: lines = lines[offset:] max_matches = head if head else 50 capped = len(lines) > max_matches if capped: lines = lines[:max_matches] if mode == 'content': formatted = [] for line in lines: if line == '--': formatted.append(line) continue for sep in (':', '-'): parts = line.split(sep, 2) if len(parts) == 3 and parts[1].isdigit(): formatted.append(f"{parts[0]}:{parts[1]:>5}| {parts[2]}") break else: formatted.append(line) lines = formatted print(f"STATUS=ok MATCH_COUNT={total}") print('\n'.join(lines)) if capped: print(f"\n(showing {max_matches} of {total} matches; use --head=N or --offset=N for more)")