Skip to content

Commit 7342eb8

Browse files
author
SqlRush
committed
Accept Grep regexp pattern aliases
1 parent 96c0aed commit 7342eb8

4 files changed

Lines changed: 66 additions & 8 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
812812

813813
本轮补充:`Grep` count 输出模式现在支持 ripgrep 风格 `count`/`--count`/`-c` 布尔参数,和 `output_mode=count` 统一归一;`--count-matches` 仍仅控制 count 模式下按 occurrence 计数,避免混淆输出模式和计数粒度。
814814

815+
本轮补充:`Grep` 搜索表达式现在除 canonical `pattern` 外,还接受 `regex``regexp``--regexp``-e` aliases;执行、校验和 structured content 都统一归一到 canonical `pattern`,便于 SDK/rg 风格调用复用同一工具。
816+
815817
本轮补充:`Grep` 路径过滤现在支持 ripgrep 风格 `--glob`/`-g``--type`/`-t` aliases,执行和 structured content 都统一使用归一化后的 glob/type 过滤值。
816818

817819
本轮补充:`Grep` 常用布尔参数继续补齐 ripgrep 长参数 aliases,覆盖 `--line-number``--ignore-case``--fixed-strings``--word-regexp``--invert-match``--only-matching`,并兼容 quoted semantic boolean。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ After the first/second batch hardening, the Go rewrite now includes initial `int
118118

119119
Covered behavior:
120120

121-
- `Glob`/`Grep` initial pure-Go file search tools, including recursive `**` glob matching, Glob absolute-pattern base-dir extraction, Glob official pattern/path-only strict schema, Glob/Grep working-directory-relative result paths, Glob default no-ignore/hidden search plus `CLAUDE_CODE_GLOB_NO_IGNORE`/`CLAUDE_CODE_GLOB_HIDDEN` env switches, Grep official VCS metadata directory exclusion (`.git`/`.svn`/`.hg`/`.bzr`/`.jj`/`.sl`), Grep hierarchical `.gitignore`/`.ignore`, Glob oldest-first modified/path sorting, Glob truncated tool-result hinting, regex and fixed-string grep (`fixed_strings`/`-F`), multiline dotall grep, glob/type filtering, Grep glob whitespace/comma multi-pattern and brace alternation, Glob/Grep path existence validation plus Glob directory-only path validation, `output_mode`/`outputMode` `files_with_matches`/`content`/`count` output modes, Grep `count`/`--count`/`-c` count-mode aliases, Grep files_with_matches file-count summaries, Grep count-mode occurrence/file summaries, Grep `--max-columns 500` long matching/context line omission placeholders, `context`/`before_context`/`after_context` and `-C`/`-B`/`-A` content context lines with official precedence and non-content modes ignoring context, `line_numbers`/`lineNumbers`/`-n` line-number control, `max_count`/`maxCount`/`-m` per-file match limiting, `offset`/`head_limit` pagination and content-mode pagination tool-result hinting, default 250-entry Grep head limit, `head_limit=0` unlimited behavior, `ignore_case`/`case_insensitive`/`caseInsensitive`/`-i` case-insensitive search aliases, and quoted semantic string coercion for Grep numeric/boolean inputs.
121+
- `Glob`/`Grep` initial pure-Go file search tools, including recursive `**` glob matching, Glob absolute-pattern base-dir extraction, Glob official pattern/path-only strict schema, Glob/Grep working-directory-relative result paths, Glob default no-ignore/hidden search plus `CLAUDE_CODE_GLOB_NO_IGNORE`/`CLAUDE_CODE_GLOB_HIDDEN` env switches, Grep official VCS metadata directory exclusion (`.git`/`.svn`/`.hg`/`.bzr`/`.jj`/`.sl`), Grep hierarchical `.gitignore`/`.ignore`, Glob oldest-first modified/path sorting, Glob truncated tool-result hinting, regex and fixed-string grep (`fixed_strings`/`-F`), Grep `regex`/`regexp`/`--regexp`/`-e` pattern aliases, multiline dotall grep, glob/type filtering, Grep glob whitespace/comma multi-pattern and brace alternation, Glob/Grep path existence validation plus Glob directory-only path validation, `output_mode`/`outputMode` `files_with_matches`/`content`/`count` output modes, Grep `count`/`--count`/`-c` count-mode aliases, Grep files_with_matches file-count summaries, Grep count-mode occurrence/file summaries, Grep `--max-columns 500` long matching/context line omission placeholders, `context`/`before_context`/`after_context` and `-C`/`-B`/`-A` content context lines with official precedence and non-content modes ignoring context, `line_numbers`/`lineNumbers`/`-n` line-number control, `max_count`/`maxCount`/`-m` per-file match limiting, `offset`/`head_limit` pagination and content-mode pagination tool-result hinting, default 250-entry Grep head limit, `head_limit=0` unlimited behavior, `ignore_case`/`case_insensitive`/`caseInsensitive`/`-i` case-insensitive search aliases, and quoted semantic string coercion for Grep numeric/boolean inputs.
122122
- `Bash` initial shell execution, including command/timeout/description validation, `/bin/sh -c` execution, stdout/stderr/exit-code/timeout structured results, dynamic read-only/concurrency-safe/destructive classification, Git diff/log/show/status/ls-files/grep/rev-parse/branch/tag/ls-remote safe-flag validation, Git remote/push/reflog/stash/worktree/merge-base/describe/cat-file/for-each-ref/rev-list/blame/shortlog/config-get argument-aware safety classification, destructive `find -delete/-exec rm` and `xargs rm` classification, safe-wrapper/env prefix normalization for Bash read-only/destructive classification (`time`/`nohup`/`timeout`/`nice`/`stdbuf`/`env`), destructive command detection after temporary environment assignments, permission-rule matching through the existing tool permission adapter, background command start, same-session `BashOutput` reads, and `KillBash` cancellation.
123123
- `TodoWrite` initial todo state, including full-list writes, status/priority validation, duplicate-id rejection, one `in_progress` guard, structured result payloads, tool metadata state storage, and session-scoped local persistence/restore.
124124
- `WebFetch` initial URL fetch behavior, including URL/timeout/max-byte validation, HTTP GET, HEAD preflight, metadata/raw `skipWebFetchPreflight` skip-preflight, binary preflight GET skipping from content type or attachment filename, text/binary detection, truncation, non-2xx error marking, structured result payloads, HTML-to-text rendering, prompt-focused excerpts, prompt phrase scoring/metadata, and `WebFetch(domain:...)` permission-rule adaptation.

internal/tools/file/search_tools.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const grepOmittedLongContextLine = "[Omitted long context line]"
2727
var semanticNumberLiteralRE = regexp.MustCompile(`^-?\d+(\.\d+)?$`)
2828

2929
var allowedGrepInputKeys = map[string]struct{}{
30-
"pattern": {}, "path": {}, "glob": {}, "--glob": {}, "-g": {}, "type": {}, "--type": {}, "-t": {}, "output_mode": {}, "outputMode": {}, "limit": {},
30+
"pattern": {}, "regex": {}, "regexp": {}, "--regexp": {}, "-e": {}, "path": {}, "glob": {}, "--glob": {}, "-g": {}, "type": {}, "--type": {}, "-t": {}, "output_mode": {}, "outputMode": {}, "limit": {},
3131
"head_limit": {}, "headLimit": {}, "offset": {}, "max_count": {}, "maxCount": {}, "-m": {},
3232
"max_columns": {}, "maxColumns": {}, "max-columns": {}, "--max-columns": {},
3333
"context": {}, "-C": {}, "before_context": {}, "beforeContext": {}, "-B": {}, "after_context": {}, "afterContext": {}, "-A": {}, "line_numbers": {}, "lineNumbers": {}, "line-number": {}, "--line-number": {}, "-n": {},
@@ -70,6 +70,10 @@ type globInput struct {
7070

7171
type grepInput struct {
7272
Pattern string `json:"pattern"`
73+
Regex string `json:"regex,omitempty"`
74+
Regexp string `json:"regexp,omitempty"`
75+
LongRegexp string `json:"--regexp,omitempty"`
76+
ShortRegexp string `json:"-e,omitempty"`
7377
Path string `json:"path,omitempty"`
7478
Glob string `json:"glob,omitempty"`
7579
LongGlob string `json:"--glob,omitempty"`
@@ -241,10 +245,13 @@ func NewGrepTool() tool.Tool {
241245
ConcurrencySafe: true,
242246
Strict: true,
243247
InputSchema: contracts.JSONSchema{
244-
"type": "object",
245-
"required": []any{"pattern"},
248+
"type": "object",
246249
"properties": map[string]any{
247250
"pattern": map[string]any{"type": "string"},
251+
"regex": map[string]any{"type": "string"},
252+
"regexp": map[string]any{"type": "string"},
253+
"--regexp": map[string]any{"type": "string"},
254+
"-e": map[string]any{"type": "string"},
248255
"path": map[string]any{"type": "string"},
249256
"glob": map[string]any{"type": "string"},
250257
"--glob": map[string]any{"type": "string"},
@@ -376,7 +383,7 @@ func NewGrepTool() tool.Tool {
376383
},
377384
},
378385
PromptFunc: func(tool.PromptContext) (string, error) {
379-
return "Searches text files under path using a regular expression or fixed string. output_mode may be files_with_matches, files_without_matches, content, or count; glob/-g/--glob and type/-t/--type optionally filter file paths. glob accepts whitespace/comma-separated patterns and brace alternation. content mode supports context, before_context, after_context, -C, -B, -A, -n/--line-number line-number control, offset, head_limit pagination, max_count/-m per-file match limiting, max_columns/--max-columns long-line omission, and only_matching/-o/--only-matching matched-text output. Use files_with_matches or -l to list files with matches, files_without_match or -L to list files without matches, and count/--count/-c for count mode. Count mode supports count_matches/--count-matches for occurrence counts. Use fixed_strings/-F/--fixed-strings for literal matching, word_regexp/-w/--word-regexp for whole-word matches, ignore_case/-i/--ignore-case for case-insensitive search, and invert_match/-v/--invert-match to select non-matching lines. Set no_ignore/--no-ignore to skip .gitignore/.ignore files while still excluding VCS metadata and read-denied paths. Set multiline to allow patterns to span lines with dot matching newlines.", nil
386+
return "Searches text files under path using a regular expression or fixed string. pattern is the canonical search expression; regex/regexp/--regexp/-e are accepted aliases. output_mode may be files_with_matches, files_without_matches, content, or count; glob/-g/--glob and type/-t/--type optionally filter file paths. glob accepts whitespace/comma-separated patterns and brace alternation. content mode supports context, before_context, after_context, -C, -B, -A, -n/--line-number line-number control, offset, head_limit pagination, max_count/-m per-file match limiting, max_columns/--max-columns long-line omission, and only_matching/-o/--only-matching matched-text output. Use files_with_matches or -l to list files with matches, files_without_match or -L to list files without matches, and count/--count/-c for count mode. Count mode supports count_matches/--count-matches for occurrence counts. Use fixed_strings/-F/--fixed-strings for literal matching, word_regexp/-w/--word-regexp for whole-word matches, ignore_case/-i/--ignore-case for case-insensitive search, and invert_match/-v/--invert-match to select non-matching lines. Set no_ignore/--no-ignore to skip .gitignore/.ignore files while still excluding VCS metadata and read-denied paths. Set multiline to allow patterns to span lines with dot matching newlines.", nil
380387
},
381388
NormalizeFunc: normalizeGrepRawInput,
382389
ValidateFunc: validateGrep,
@@ -446,7 +453,7 @@ func validateGrep(ctx tool.Context, raw json.RawMessage) error {
446453
if err != nil {
447454
return err
448455
}
449-
if strings.TrimSpace(input.Pattern) == "" {
456+
if strings.TrimSpace(grepPattern(input)) == "" {
450457
return fmt.Errorf("pattern is required")
451458
}
452459
if _, err := compileGrepPattern(input); err != nil {
@@ -560,7 +567,7 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
560567
Content: content,
561568
StructuredContent: map[string]any{
562569
"type": "grep",
563-
"pattern": input.Pattern,
570+
"pattern": grepPattern(input),
564571
"path": input.Path,
565572
"glob": globFilter,
566573
"type_filter": typeFilter,
@@ -1219,8 +1226,17 @@ func normalizedGrepOutputMode(input grepInput) string {
12191226
return mode
12201227
}
12211228

1229+
func grepPattern(input grepInput) string {
1230+
for _, pattern := range []string{input.Pattern, input.Regex, input.Regexp, input.LongRegexp, input.ShortRegexp} {
1231+
if strings.TrimSpace(pattern) != "" {
1232+
return pattern
1233+
}
1234+
}
1235+
return ""
1236+
}
1237+
12221238
func compileGrepPattern(input grepInput) (*regexp.Regexp, error) {
1223-
pattern := input.Pattern
1239+
pattern := grepPattern(input)
12241240
if grepFixedStrings(input) {
12251241
pattern = regexp.QuoteMeta(pattern)
12261242
}

internal/tools/file/tools_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,46 @@ func TestGrepToolOutputModesAndGlobFilter(t *testing.T) {
13171317
t.Fatalf("content result = %#v", contentResult.Content)
13181318
}
13191319

1320+
regexpAliasResult, err := executor.Execute(ctx, contracts.ToolUse{
1321+
ID: "toolu_grep_regexp_alias",
1322+
Name: "Grep",
1323+
Input: json.RawMessage(`{"--regexp":"Alpha","glob":"**/*.go","output_mode":"content"}`),
1324+
}, nil)
1325+
if err != nil {
1326+
t.Fatal(err)
1327+
}
1328+
if regexpAliasResult.Content != "src/a.go:2:func Alpha() {}\nsrc/c.go:2:func AlphaBeta() {}" ||
1329+
regexpAliasResult.StructuredContent["pattern"] != "Alpha" {
1330+
t.Fatalf("regexp alias result = %#v", regexpAliasResult)
1331+
}
1332+
1333+
regexAliasResult, err := executor.Execute(ctx, contracts.ToolUse{
1334+
ID: "toolu_grep_regex_alias",
1335+
Name: "Grep",
1336+
Input: json.RawMessage(`{"regex":"Beta","glob":"**/*.go"}`),
1337+
}, nil)
1338+
if err != nil {
1339+
t.Fatal(err)
1340+
}
1341+
if regexAliasResult.Content != "Found 1 file\nsrc/c.go" ||
1342+
regexAliasResult.StructuredContent["pattern"] != "Beta" {
1343+
t.Fatalf("regex alias result = %#v", regexAliasResult)
1344+
}
1345+
1346+
shortRegexpAliasResult, err := executor.Execute(ctx, contracts.ToolUse{
1347+
ID: "toolu_grep_short_regexp_alias",
1348+
Name: "Grep",
1349+
Input: json.RawMessage(`{"-e":"Alpha","glob":"**/*.go","output_mode":"count"}`),
1350+
}, nil)
1351+
if err != nil {
1352+
t.Fatal(err)
1353+
}
1354+
wantRegexpAliasCount := "src/a.go:1\nsrc/c.go:1\n\nFound 2 total occurrences across 2 files."
1355+
if shortRegexpAliasResult.Content != wantRegexpAliasCount ||
1356+
shortRegexpAliasResult.StructuredContent["pattern"] != "Alpha" {
1357+
t.Fatalf("short regexp alias result = %#v", shortRegexpAliasResult)
1358+
}
1359+
13201360
countResult, err := executor.Execute(ctx, contracts.ToolUse{
13211361
ID: "toolu_grep_count",
13221362
Name: "Grep",

0 commit comments

Comments
 (0)