feat: enhance GrepTool with output_mode, case_insensitive, multiline, context lines
Add 7 new parameters to match TS GrepTool capabilities: - output_mode: content/files_with_matches/count (3 modes like TS) - case_insensitive: -i flag support - multiline: cross-line pattern matching (ripgrep only) - context_before/context_after: -B/-A context lines - type: file type filter (e.g., 'java', 'py') for ripgrep - head_limit: configurable result limit (was hardcoded 100) All parameters correctly wired to rg, grep, and findstr backends. Description updated with output mode documentation and escaping notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,6 @@ import com.claudecode.tool.Tool;
|
|||||||
import com.claudecode.tool.ToolContext;
|
import com.claudecode.tool.ToolContext;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -16,10 +15,11 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* Grep 搜索工具 —— 对应 claude-code/src/tools/grep/GrepTool.ts。
|
* Grep 搜索工具 —— 对应 claude-code/src/tools/grep/GrepTool.ts。
|
||||||
* <p>
|
* <p>
|
||||||
* 在文件中搜索文本模式(正则),优先使用 ripgrep(rg),降级为系统 grep。
|
* 在文件中搜索文本模式(正则),优先使用 ripgrep(rg),降级为系统 grep。
|
||||||
|
* 支持多种输出模式、大小写、上下文行、多行匹配等参数。
|
||||||
*/
|
*/
|
||||||
public class GrepTool implements Tool {
|
public class GrepTool implements Tool {
|
||||||
|
|
||||||
private static final int MAX_RESULTS = 100;
|
private static final int DEFAULT_MAX_RESULTS = 100;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String name() {
|
public String name() {
|
||||||
@@ -37,10 +37,14 @@ public class GrepTool implements Tool {
|
|||||||
better understand and review your searches.
|
better understand and review your searches.
|
||||||
|
|
||||||
Uses ripgrep (rg) if available, falls back to system grep/findstr. Supports full regex \
|
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, \
|
Output modes:
|
||||||
consider using the Agent tool instead to keep the main context clean.""";
|
- "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
|
@Override
|
||||||
@@ -60,6 +64,35 @@ public class GrepTool implements Tool {
|
|||||||
"include": {
|
"include": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "File glob pattern to include (e.g., '*.java')"
|
"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"]
|
"required": ["pattern"]
|
||||||
@@ -76,10 +109,20 @@ public class GrepTool implements Tool {
|
|||||||
String pattern = (String) input.get("pattern");
|
String pattern = (String) input.get("pattern");
|
||||||
String searchPath = (String) input.getOrDefault("path", ".");
|
String searchPath = (String) input.getOrDefault("path", ".");
|
||||||
String include = (String) input.getOrDefault("include", null);
|
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();
|
Path baseDir = context.getWorkDir().resolve(searchPath).normalize();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<String> cmd = buildCommand(pattern, baseDir.toString(), include);
|
List<String> cmd = buildCommand(pattern, baseDir.toString(), include, type,
|
||||||
|
outputMode, caseInsensitive, multiline, contextBefore, contextAfter);
|
||||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||||
pb.directory(context.getWorkDir().toFile());
|
pb.directory(context.getWorkDir().toFile());
|
||||||
pb.redirectErrorStream(true);
|
pb.redirectErrorStream(true);
|
||||||
@@ -89,7 +132,7 @@ public class GrepTool implements Tool {
|
|||||||
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null && lines.size() < MAX_RESULTS) {
|
while ((line = reader.readLine()) != null && lines.size() < headLimit) {
|
||||||
lines.add(line);
|
lines.add(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,8 +147,8 @@ public class GrepTool implements Tool {
|
|||||||
for (String line : lines) {
|
for (String line : lines) {
|
||||||
sb.append(line).append("\n");
|
sb.append(line).append("\n");
|
||||||
}
|
}
|
||||||
if (lines.size() >= MAX_RESULTS) {
|
if (lines.size() >= headLimit) {
|
||||||
sb.append("... (results truncated at ").append(MAX_RESULTS).append(")\n");
|
sb.append("... (results truncated at ").append(headLimit).append(")\n");
|
||||||
}
|
}
|
||||||
return sb.toString().stripTrailing();
|
return sb.toString().stripTrailing();
|
||||||
|
|
||||||
@@ -115,28 +158,66 @@ public class GrepTool implements Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 构建搜索命令(优先 rg,降级 grep/findstr) */
|
/** 构建搜索命令(优先 rg,降级 grep/findstr) */
|
||||||
private List<String> buildCommand(String pattern, String path, String include) {
|
private List<String> 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");
|
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
|
||||||
List<String> cmd = new ArrayList<>();
|
List<String> cmd = new ArrayList<>();
|
||||||
|
|
||||||
// 尝试 ripgrep
|
|
||||||
if (isCommandAvailable("rg")) {
|
if (isCommandAvailable("rg")) {
|
||||||
cmd.add("rg");
|
cmd.add("rg");
|
||||||
cmd.add("--no-heading");
|
cmd.add("--no-heading");
|
||||||
cmd.add("--line-number");
|
|
||||||
cmd.add("--color=never");
|
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) {
|
if (include != null) {
|
||||||
cmd.add("--glob=" + include);
|
cmd.add("--glob=" + include);
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.add(pattern);
|
cmd.add(pattern);
|
||||||
cmd.add(path);
|
cmd.add(path);
|
||||||
|
|
||||||
} else if (isWindows) {
|
} else if (isWindows) {
|
||||||
// Windows 降级到 findstr(功能有限)
|
// Windows fallback: findstr (limited functionality)
|
||||||
cmd.add("findstr");
|
cmd.add("findstr");
|
||||||
cmd.add("/s");
|
cmd.add("/s");
|
||||||
cmd.add("/n");
|
cmd.add("/n");
|
||||||
cmd.add("/r");
|
cmd.add("/r");
|
||||||
|
if (caseInsensitive) {
|
||||||
|
cmd.add("/i");
|
||||||
|
}
|
||||||
cmd.add(pattern);
|
cmd.add(pattern);
|
||||||
if (include != null) {
|
if (include != null) {
|
||||||
cmd.add(path + "\\" + include);
|
cmd.add(path + "\\" + include);
|
||||||
@@ -144,9 +225,30 @@ public class GrepTool implements Tool {
|
|||||||
cmd.add(path + "\\*");
|
cmd.add(path + "\\*");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Unix fallback: grep
|
||||||
cmd.add("grep");
|
cmd.add("grep");
|
||||||
cmd.add("-rn");
|
cmd.add("-rn");
|
||||||
cmd.add("--color=never");
|
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) {
|
if (include != null) {
|
||||||
cmd.add("--include=" + include);
|
cmd.add("--include=" + include);
|
||||||
}
|
}
|
||||||
@@ -157,6 +259,14 @@ public class GrepTool implements Tool {
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Integer getInt(Map<String, Object> input, String key) {
|
||||||
|
Object val = input.get(key);
|
||||||
|
if (val instanceof Number n) {
|
||||||
|
return n.intValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCommandAvailable(String command) {
|
private boolean isCommandAvailable(String command) {
|
||||||
try {
|
try {
|
||||||
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
|
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
|
||||||
|
|||||||
Reference in New Issue
Block a user