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>
pull/1/head
abel533 1 month ago
parent c5440a2b4c
commit 08ebc72416
  1. 138
      src/main/java/com/claudecode/tool/impl/GrepTool.java

@ -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>
* 在文件中搜索文本模式正则优先使用 ripgreprg降级为系统 grep * 在文件中搜索文本模式正则优先使用 ripgreprg降级为系统 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");

Loading…
Cancel
Save