mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-09 06:05:00 +00:00
Add a Grep tool
This commit is contained in:
parent
b3dbb79795
commit
dc2f8a9cf1
4 changed files with 176 additions and 2 deletions
|
@ -762,6 +762,21 @@ class NavigatorCoder(Coder):
|
|||
else:
|
||||
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
|
||||
elif norm_tool_name == 'replacetext':
|
||||
file_path = params.get('file_path')
|
||||
|
|
|
@ -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.**
|
||||
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
|
||||
- **View**: `[tool_call(View, file_path="src/main.py")]`
|
||||
Explicitly add a specific file to context as read-only.
|
||||
|
|
|
@ -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.**
|
||||
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
|
||||
- **View**: `[tool_call(View, file_path="src/main.py")]`
|
||||
Explicitly add a specific file to context as read-only.
|
||||
|
|
145
aider/tools/grep.py
Normal file
145
aider/tools/grep.py
Normal 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)}"
|
Loading…
Add table
Add a link
Reference in a new issue