#!/usr/bin/env python3 # ollie:parallel read # ollie:prompt # ## file_grep # # Search file contents with ripgrep. Capped at 50 matches by default. # # **Args**: `[pattern, ...flags]` # # - First arg (required): regex search pattern (positional). # - Remaining args: flags only. The search path is a **flag**, not a positional arg. # # ``` # call_tool: calls=[{tool: "file_grep", args: ["pattern", "--path=/abs/dir"]}] # call_tool: calls=[{tool: "file_grep", args: ["TODO", "--path=src/", "--glob=*.go", "-i"]}] # call_tool: calls=[{tool: "file_grep", args: ["func main", "--path=/home/user/project"]}] # ``` # # - BAD: `args: ["pattern", "/path/to/dir"]` — path must use `--path=` flag # - GOOD: `args: ["pattern", "--path=/path/to/dir"]` # # **Flags**: `--path=`, `--mode=files_with_matches|content|count`, `--glob=`, `--type=`, `-i`, `-A `, `-B `, `-C `, `--multiline`, `--head=`, `--offset=`. # ollie:end import sys import os import subprocess import shutil import argparse parser = argparse.ArgumentParser(prog='file_grep', add_help=False) parser.add_argument('pattern') parser.add_argument('--path', default='') parser.add_argument('--mode', default='content', choices=['files_with_matches', 'content', 'count']) parser.add_argument('--glob', default='') parser.add_argument('--type', default='') parser.add_argument('-i', action='store_true') parser.add_argument('-n', action='store_true', default=True) parser.add_argument('-A', type=int, default=None) parser.add_argument('-B', type=int, default=None) parser.add_argument('-C', type=int, default=None) parser.add_argument('--multiline', action='store_true') parser.add_argument('--head', type=int, default=0) parser.add_argument('--offset', type=int, default=0) args = parser.parse_args() search_path = args.path or os.getcwd() use_rg = shutil.which('rg') is not None if use_rg: cmd = ['rg', '--no-config', '--color=never'] if args.mode == 'files_with_matches': cmd.append('-l') elif args.mode == 'count': cmd.append('--count') elif args.mode == 'content': if args.n: cmd.append('-n') if args.i: cmd.append('-i') if args.multiline: cmd += ['-U', '--multiline-dotall'] if args.glob: cmd += ['-g', args.glob] if args.type: cmd += ['-t', args.type] if args.C is not None: cmd += ['-C', str(args.C)] if args.A is not None: cmd += ['-A', str(args.A)] if args.B is not None: cmd += ['-B', str(args.B)] cmd += ['-e', args.pattern, search_path] else: # Fallback to GNU grep -r cmd = ['grep', '-r', '--color=never'] if args.mode == 'files_with_matches': cmd.append('-l') elif args.mode == 'count': cmd.append('-c') elif args.mode == 'content': if args.n: cmd.append('-n') if args.i: cmd.append('-i') if args.glob: cmd += ['--include=' + args.glob] if args.C is not None: cmd += ['-C', str(args.C)] if args.A is not None: cmd += ['-A', str(args.A)] if args.B is not None: cmd += ['-B', str(args.B)] cmd += ['-e', args.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 args.offset: lines = lines[args.offset:] max_matches = args.head if args.head else 50 capped = len(lines) > max_matches if capped: lines = lines[:max_matches] if args.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)")