ollie/data/tools/file_grep

137 lines
4.6 KiB
Python
Executable File

#!/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.environ.get('OLLIE_TOOLS_PATH', 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)")