Add a Grep tool

This commit is contained in:
Amar Sood (tekacs) 2025-04-12 13:37:28 -04:00
parent b3dbb79795
commit dc2f8a9cf1
4 changed files with 176 additions and 2 deletions

View file

@ -762,6 +762,21 @@ class NavigatorCoder(Coder):
else: else:
result_message = "Error: Missing 'command_string' parameter for CommandInteractive" result_message = "Error: Missing 'command_string' parameter for CommandInteractive"
# Grep tool
elif norm_tool_name == 'grep':
pattern = params.get('pattern')
file_pattern = params.get('file_pattern', '*') # Default to all files
directory = params.get('directory', '.') # Default to current directory
use_regex = params.get('use_regex', False) # Default to literal search
case_insensitive = params.get('case_insensitive', False) # Default to case-sensitive
if pattern is not None:
# Import the function if not already imported (it should be)
from aider.tools.grep import _execute_grep
result_message = _execute_grep(self, pattern, file_pattern, directory, use_regex, case_insensitive)
else:
result_message = "Error: Missing required 'pattern' parameter for Grep"
# Granular editing tools # Granular editing tools
elif norm_tool_name == 'replacetext': elif norm_tool_name == 'replacetext':
file_path = params.get('file_path') file_path = params.get('file_path')

View file

@ -52,6 +52,13 @@ Act as an expert software engineer with the ability to autonomously navigate and
Find files containing a specific symbol (function, class, variable). **Found files are automatically added to context as read-only.** Find files containing a specific symbol (function, class, variable). **Found files are automatically added to context as read-only.**
Leverages the repo map for accurate symbol lookup. Leverages the repo map for accurate symbol lookup.
- **Grep**: `[tool_call(Grep, pattern="my_variable", file_pattern="*.py", directory="src", use_regex=False, case_insensitive=False)]`
Search for lines matching a pattern in files using the best available tool (`rg`, `ag`, or `grep`). Returns matching lines with line numbers.
`file_pattern` (optional, default "*") filters files using glob syntax.
`directory` (optional, default ".") specifies the search directory relative to the repo root.
`use_regex` (optional, default False): If False, performs a literal/fixed string search. If True, uses basic Extended Regular Expression (ERE) syntax.
`case_insensitive` (optional, default False): If False (default), the search is case-sensitive. If True, the search is case-insensitive.
### Context Management Tools ### Context Management Tools
- **View**: `[tool_call(View, file_path="src/main.py")]` - **View**: `[tool_call(View, file_path="src/main.py")]`
Explicitly add a specific file to context as read-only. Explicitly add a specific file to context as read-only.

View file

@ -52,6 +52,13 @@ Act as an expert software engineer with the ability to autonomously navigate and
Find files containing a specific symbol (function, class, variable). **Found files are automatically added to context as read-only.** Find files containing a specific symbol (function, class, variable). **Found files are automatically added to context as read-only.**
Leverages the repo map for accurate symbol lookup. Leverages the repo map for accurate symbol lookup.
- **Grep**: `[tool_call(Grep, pattern="my_variable", file_pattern="*.py", directory="src", use_regex=False, case_insensitive=False)]`
Search for lines matching a pattern in files using the best available tool (`rg`, `ag`, or `grep`). Returns matching lines with line numbers.
`file_pattern` (optional, default "*") filters files using glob syntax.
`directory` (optional, default ".") specifies the search directory relative to the repo root.
`use_regex` (optional, default False): If False, performs a literal/fixed string search. If True, uses basic Extended Regular Expression (ERE) syntax.
`case_insensitive` (optional, default False): If False (default), the search is case-sensitive. If True, the search is case-insensitive.
### Context Management Tools ### Context Management Tools
- **View**: `[tool_call(View, file_path="src/main.py")]` - **View**: `[tool_call(View, file_path="src/main.py")]`
Explicitly add a specific file to context as read-only. Explicitly add a specific file to context as read-only.

145
aider/tools/grep.py Normal file
View file

@ -0,0 +1,145 @@
import shlex
import shutil
from pathlib import Path
from aider.run_cmd import run_cmd_subprocess
def _find_search_tool():
"""Find the best available command-line search tool (rg, ag, grep)."""
if shutil.which('rg'):
return 'rg', shutil.which('rg')
elif shutil.which('ag'):
return 'ag', shutil.which('ag')
elif shutil.which('grep'):
return 'grep', shutil.which('grep')
else:
return None, None
def _execute_grep(coder, pattern, file_pattern="*", directory=".", use_regex=False, case_insensitive=False):
"""
Search for lines matching a pattern in files within the project repository.
Uses rg (ripgrep), ag (the silver searcher), or grep, whichever is available.
Args:
coder: The Coder instance.
pattern (str): The pattern to search for.
file_pattern (str, optional): Glob pattern to filter files. Defaults to "*".
directory (str, optional): Directory to search within relative to repo root. Defaults to ".".
use_regex (bool, optional): Whether the pattern is a regular expression. Defaults to False.
Returns:
str: Formatted result indicating success or failure, including matching lines or error message.
"""
repo = coder.repo
if not repo:
coder.io.tool_error("Not in a git repository.")
return "Error: Not in a git repository."
tool_name, tool_path = _find_search_tool()
if not tool_path:
coder.io.tool_error("No search tool (rg, ag, grep) found in PATH.")
return "Error: No search tool (rg, ag, grep) found."
try:
search_dir_path = Path(repo.root) / directory
if not search_dir_path.is_dir():
coder.io.tool_error(f"Directory not found: {directory}")
return f"Error: Directory not found: {directory}"
# Build the command arguments based on the available tool
cmd_args = [tool_path]
# Common options or tool-specific equivalents
if tool_name in ['rg', 'grep']:
cmd_args.append("-n") # Line numbers for rg and grep
# ag includes line numbers by default
# Case sensitivity
if case_insensitive:
cmd_args.append("-i") # Add case-insensitivity flag for all tools
# Pattern type (regex vs fixed string)
if use_regex:
if tool_name == 'grep':
cmd_args.append("-E") # Use extended regex for grep
# rg and ag use regex by default, no flag needed for basic ERE
else:
if tool_name == 'rg':
cmd_args.append("-F") # Fixed strings for rg
elif tool_name == 'ag':
cmd_args.append("-Q") # Literal/fixed strings for ag
elif tool_name == 'grep':
cmd_args.append("-F") # Fixed strings for grep
# File filtering
if file_pattern != "*": # Avoid adding glob if it's the default '*' which might behave differently
if tool_name == 'rg':
cmd_args.extend(["-g", file_pattern])
elif tool_name == 'ag':
cmd_args.extend(["-G", file_pattern])
elif tool_name == 'grep':
# grep needs recursive flag when filtering
cmd_args.append("-r")
cmd_args.append(f"--include={file_pattern}")
elif tool_name == 'grep':
# grep needs recursive flag even without include filter
cmd_args.append("-r")
# Directory exclusion (rg and ag respect .gitignore/.git by default)
if tool_name == 'grep':
cmd_args.append("--exclude-dir=.git")
# Add pattern and directory path
cmd_args.extend([pattern, str(search_dir_path)])
# Convert list to command string for run_cmd_subprocess
command_string = shlex.join(cmd_args)
coder.io.tool_output(f"⚙️ Executing {tool_name}: {command_string}")
# Use run_cmd_subprocess for execution
# Note: rg, ag, and grep return 1 if no matches are found, which is not an error for this tool.
exit_status, combined_output = run_cmd_subprocess(
command_string,
verbose=coder.verbose,
cwd=coder.root # Execute in the project root
)
# Format the output for the result message
output_content = combined_output or ""
# Handle exit codes (consistent across rg, ag, grep)
if exit_status == 0:
# Limit output size if necessary
max_output_lines = 50 # Consider making this configurable
output_lines = output_content.splitlines()
if len(output_lines) > max_output_lines:
truncated_output = "\n".join(output_lines[:max_output_lines])
result_message = f"Found matches (truncated):\n```text\n{truncated_output}\n... ({len(output_lines) - max_output_lines} more lines)\n```"
elif not output_content:
# Should not happen if return code is 0, but handle defensively
coder.io.tool_warning(f"{tool_name} returned 0 but produced no output.")
result_message = "No matches found (unexpected)."
else:
result_message = f"Found matches:\n```text\n{output_content}\n```"
return result_message
elif exit_status == 1:
# Exit code 1 means no matches found - this is expected behavior, not an error.
return "No matches found."
else:
# Exit code > 1 indicates an actual error
error_message = f"{tool_name.capitalize()} command failed with exit code {exit_status}."
if output_content:
# Truncate error output as well if it's too long
error_limit = 1000 # Example limit for error output
if len(output_content) > error_limit:
output_content = output_content[:error_limit] + "\n... (error output truncated)"
error_message += f" Output:\n{output_content}"
coder.io.tool_error(error_message)
return f"Error: {error_message}"
except Exception as e:
# Add command_string to the error message if it's defined
cmd_str_info = f"'{command_string}' " if 'command_string' in locals() else ""
coder.io.tool_error(f"Error executing {tool_name} command {cmd_str_info}: {str(e)}")
return f"Error executing {tool_name}: {str(e)}"