diff --git a/src/main/java/com/claudecode/tool/impl/GrepTool.java b/src/main/java/com/claudecode/tool/impl/GrepTool.java index cd9281e..77ffb16 100644 --- a/src/main/java/com/claudecode/tool/impl/GrepTool.java +++ b/src/main/java/com/claudecode/tool/impl/GrepTool.java @@ -4,7 +4,6 @@ import com.claudecode.tool.Tool; import com.claudecode.tool.ToolContext; import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Path; import java.util.ArrayList; @@ -16,10 +15,11 @@ import java.util.concurrent.TimeUnit; * Grep 搜索工具 —— 对应 claude-code/src/tools/grep/GrepTool.ts。 *

* 在文件中搜索文本模式(正则),优先使用 ripgrep(rg),降级为系统 grep。 + * 支持多种输出模式、大小写、上下文行、多行匹配等参数。 */ public class GrepTool implements Tool { - private static final int MAX_RESULTS = 100; + private static final int DEFAULT_MAX_RESULTS = 100; @Override public String name() { @@ -37,10 +37,14 @@ public class GrepTool implements Tool { better understand and review your searches. Uses ripgrep (rg) if available, falls back to system grep/findstr. Supports full regex \ - syntax. Use the 'include' parameter to filter by file type (e.g., '*.java', '*.ts'). + syntax. Pattern syntax uses ripgrep — literal braces need escaping (e.g., interface\\{\\}). - When you are doing an open-ended search that may require multiple rounds of searching, \ - consider using the Agent tool instead to keep the main context clean."""; + Output modes: + - "content": Shows matching lines with context (default). Supports context flags. + - "files_with_matches": Shows only file paths containing matches. Use for broad discovery. + - "count": Shows match counts per file. + + When doing open-ended searches requiring multiple rounds, use the Agent tool instead."""; } @Override @@ -60,6 +64,35 @@ public class GrepTool implements Tool { "include": { "type": "string", "description": "File glob pattern to include (e.g., '*.java')" + }, + "type": { + "type": "string", + "description": "File type filter (e.g., 'java', 'py', 'ts', 'js'). Only works with ripgrep." + }, + "output_mode": { + "type": "string", + "enum": ["content", "files_with_matches", "count"], + "description": "Output format. 'content' shows matching lines (default), 'files_with_matches' shows only file paths, 'count' shows match counts per file." + }, + "case_insensitive": { + "type": "boolean", + "description": "Case insensitive search (default: false)" + }, + "multiline": { + "type": "boolean", + "description": "Enable multiline mode where patterns can span lines (default: false)" + }, + "context_before": { + "type": "integer", + "description": "Lines of context before each match" + }, + "context_after": { + "type": "integer", + "description": "Lines of context after each match" + }, + "head_limit": { + "type": "integer", + "description": "Limit output to first N results" } }, "required": ["pattern"] @@ -76,10 +109,20 @@ public class GrepTool implements Tool { String pattern = (String) input.get("pattern"); String searchPath = (String) input.getOrDefault("path", "."); String include = (String) input.getOrDefault("include", null); + String type = (String) input.getOrDefault("type", null); + String outputMode = (String) input.getOrDefault("output_mode", "content"); + boolean caseInsensitive = Boolean.TRUE.equals(input.get("case_insensitive")); + boolean multiline = Boolean.TRUE.equals(input.get("multiline")); + Integer contextBefore = getInt(input, "context_before"); + Integer contextAfter = getInt(input, "context_after"); + int headLimit = getInt(input, "head_limit") != null + ? getInt(input, "head_limit") : DEFAULT_MAX_RESULTS; + Path baseDir = context.getWorkDir().resolve(searchPath).normalize(); try { - List cmd = buildCommand(pattern, baseDir.toString(), include); + List cmd = buildCommand(pattern, baseDir.toString(), include, type, + outputMode, caseInsensitive, multiline, contextBefore, contextAfter); ProcessBuilder pb = new ProcessBuilder(cmd); pb.directory(context.getWorkDir().toFile()); pb.redirectErrorStream(true); @@ -89,7 +132,7 @@ public class GrepTool implements Tool { try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; - while ((line = reader.readLine()) != null && lines.size() < MAX_RESULTS) { + while ((line = reader.readLine()) != null && lines.size() < headLimit) { lines.add(line); } } @@ -104,8 +147,8 @@ public class GrepTool implements Tool { for (String line : lines) { sb.append(line).append("\n"); } - if (lines.size() >= MAX_RESULTS) { - sb.append("... (results truncated at ").append(MAX_RESULTS).append(")\n"); + if (lines.size() >= headLimit) { + sb.append("... (results truncated at ").append(headLimit).append(")\n"); } return sb.toString().stripTrailing(); @@ -115,28 +158,66 @@ public class GrepTool implements Tool { } /** 构建搜索命令(优先 rg,降级 grep/findstr) */ - private List buildCommand(String pattern, String path, String include) { + private List buildCommand(String pattern, String path, String include, + String type, String outputMode, boolean caseInsensitive, + boolean multiline, Integer contextBefore, Integer contextAfter) { boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); List cmd = new ArrayList<>(); - // 尝试 ripgrep if (isCommandAvailable("rg")) { cmd.add("rg"); cmd.add("--no-heading"); - cmd.add("--line-number"); cmd.add("--color=never"); - cmd.add("--max-count=100"); + + // Output mode + switch (outputMode) { + case "files_with_matches" -> cmd.add("--files-with-matches"); + case "count" -> cmd.add("--count"); + default -> cmd.add("--line-number"); + } + + // Case insensitive + if (caseInsensitive) { + cmd.add("--ignore-case"); + } + + // Multiline + if (multiline) { + cmd.add("--multiline"); + } + + // Context lines (only for content mode) + if ("content".equals(outputMode)) { + if (contextBefore != null) { + cmd.add("--before-context=" + contextBefore); + } + if (contextAfter != null) { + cmd.add("--after-context=" + contextAfter); + } + } + + // File type filter + if (type != null) { + cmd.add("--type=" + type); + } + + // Include glob if (include != null) { cmd.add("--glob=" + include); } + cmd.add(pattern); cmd.add(path); + } else if (isWindows) { - // Windows 降级到 findstr(功能有限) + // Windows fallback: findstr (limited functionality) cmd.add("findstr"); cmd.add("/s"); cmd.add("/n"); cmd.add("/r"); + if (caseInsensitive) { + cmd.add("/i"); + } cmd.add(pattern); if (include != null) { cmd.add(path + "\\" + include); @@ -144,9 +225,30 @@ public class GrepTool implements Tool { cmd.add(path + "\\*"); } } else { + // Unix fallback: grep cmd.add("grep"); cmd.add("-rn"); cmd.add("--color=never"); + + if (caseInsensitive) { + cmd.add("-i"); + } + + if ("files_with_matches".equals(outputMode)) { + cmd.add("-l"); + } else if ("count".equals(outputMode)) { + cmd.add("-c"); + } + + if ("content".equals(outputMode)) { + if (contextBefore != null) { + cmd.add("-B" + contextBefore); + } + if (contextAfter != null) { + cmd.add("-A" + contextAfter); + } + } + if (include != null) { cmd.add("--include=" + include); } @@ -157,6 +259,14 @@ public class GrepTool implements Tool { return cmd; } + private static Integer getInt(Map input, String key) { + Object val = input.get(key); + if (val instanceof Number n) { + return n.intValue(); + } + return null; + } + private boolean isCommandAvailable(String command) { try { boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");