135 lines
3.8 KiB
Rust
135 lines
3.8 KiB
Rust
|
|
// tools/grep.rs — Search file contents
|
||
|
|
//
|
||
|
|
// Prefers ripgrep (rg) for speed, falls back to grep -r if rg
|
||
|
|
// isn't installed. Both produce compatible output.
|
||
|
|
|
||
|
|
use anyhow::{Context, Result};
|
||
|
|
use serde_json::json;
|
||
|
|
use std::process::Command;
|
||
|
|
|
||
|
|
use crate::types::ToolDef;
|
||
|
|
|
||
|
|
pub fn definition() -> ToolDef {
|
||
|
|
ToolDef::new(
|
||
|
|
"grep",
|
||
|
|
"Search for a pattern in files. Returns matching file paths by default, \
|
||
|
|
or matching lines with context.",
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"pattern": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Regex pattern to search for"
|
||
|
|
},
|
||
|
|
"path": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Directory or file to search in (default: current directory)"
|
||
|
|
},
|
||
|
|
"glob": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Glob pattern to filter files (e.g. '*.rs', '*.py')"
|
||
|
|
},
|
||
|
|
"show_content": {
|
||
|
|
"type": "boolean",
|
||
|
|
"description": "Show matching lines instead of just file paths"
|
||
|
|
},
|
||
|
|
"context_lines": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Number of context lines around matches (requires show_content)"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["pattern"]
|
||
|
|
}),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if ripgrep is available (cached after first check).
|
||
|
|
fn has_rg() -> bool {
|
||
|
|
use std::sync::OnceLock;
|
||
|
|
static HAS_RG: OnceLock<bool> = OnceLock::new();
|
||
|
|
*HAS_RG.get_or_init(|| Command::new("rg").arg("--version").output().is_ok())
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn grep(args: &serde_json::Value) -> Result<String> {
|
||
|
|
let pattern = args["pattern"].as_str().context("pattern is required")?;
|
||
|
|
let path = args["path"].as_str().unwrap_or(".");
|
||
|
|
let file_glob = args["glob"].as_str();
|
||
|
|
let show_content = args["show_content"].as_bool().unwrap_or(false);
|
||
|
|
let context = args["context_lines"].as_u64();
|
||
|
|
|
||
|
|
let output = if has_rg() {
|
||
|
|
run_rg(pattern, path, file_glob, show_content, context)?
|
||
|
|
} else {
|
||
|
|
run_grep(pattern, path, file_glob, show_content, context)?
|
||
|
|
};
|
||
|
|
|
||
|
|
if output.is_empty() {
|
||
|
|
return Ok("No matches found.".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut result = output;
|
||
|
|
const MAX_OUTPUT: usize = 30000;
|
||
|
|
if result.len() > MAX_OUTPUT {
|
||
|
|
result.truncate(MAX_OUTPUT);
|
||
|
|
result.push_str("\n... (output truncated)");
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(result)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run_rg(
|
||
|
|
pattern: &str,
|
||
|
|
path: &str,
|
||
|
|
file_glob: Option<&str>,
|
||
|
|
show_content: bool,
|
||
|
|
context: Option<u64>,
|
||
|
|
) -> Result<String> {
|
||
|
|
let mut cmd = Command::new("rg");
|
||
|
|
|
||
|
|
if show_content {
|
||
|
|
cmd.arg("-n");
|
||
|
|
if let Some(c) = context {
|
||
|
|
cmd.arg("-C").arg(c.to_string());
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
cmd.arg("--files-with-matches");
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(g) = file_glob {
|
||
|
|
cmd.arg("--glob").arg(g);
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd.arg(pattern).arg(path);
|
||
|
|
let output = cmd.output().context("Failed to run rg")?;
|
||
|
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run_grep(
|
||
|
|
pattern: &str,
|
||
|
|
path: &str,
|
||
|
|
file_glob: Option<&str>,
|
||
|
|
show_content: bool,
|
||
|
|
context: Option<u64>,
|
||
|
|
) -> Result<String> {
|
||
|
|
let mut cmd = Command::new("grep");
|
||
|
|
cmd.arg("-r"); // recursive
|
||
|
|
|
||
|
|
if show_content {
|
||
|
|
cmd.arg("-n"); // line numbers
|
||
|
|
if let Some(c) = context {
|
||
|
|
cmd.arg("-C").arg(c.to_string());
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
cmd.arg("-l"); // files-with-matches
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(g) = file_glob {
|
||
|
|
cmd.arg("--include").arg(g);
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd.arg("-E"); // extended regex
|
||
|
|
cmd.arg(pattern).arg(path);
|
||
|
|
let output = cmd.output().context("Failed to run grep")?;
|
||
|
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||
|
|
}
|