Skip to content

Commit f50319e

Browse files
author
SqlRush
committed
Support grep filter flag aliases
1 parent 26f86c0 commit f50319e

4 files changed

Lines changed: 89 additions & 6 deletions

File tree

docs/cc-100-roadmap.md

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

785785
本轮补充:`Grep` 文件列表输出现在显式支持 `files_with_match(es)`/`filesWithMatch(es)`/`files-with-match(es)`/`--files-with-match(es)`/`-l`,并接受 `output_mode``files_with_match` alias,统一归一为 `files_with_matches`
786786

787+
本轮补充:`Grep` 路径过滤现在支持 ripgrep 风格 `--glob`/`-g``--type`/`-t` aliases,执行和 structured content 都统一使用归一化后的 glob/type 过滤值。
788+
787789
本轮补充:`Grep` 搜索现在支持 `no_ignore`/`noIgnore`/`no-ignore`/`--no-ignore`,可跳过 `.gitignore`/`.ignore` 规则,同时继续排除 VCS metadata 目录并保留 `Read(...)` deny 额外 ignore 保护;`--no-ignore` 兼容 quoted boolean。
788790

789791
本轮补充:`Grep``files_with_matches` 输出现在按官方行为使用文件修改时间倒序排序,mtime 相同再按路径排序;分页和 `head_limit` 会在排序后应用。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ test/parity/ # golden tests against TS/official behavior
188188
- 本轮补充:Grep 长行省略阈值现在支持 `max_columns`/`maxColumns`/`max-columns`/`--max-columns`,默认保持 500,传 `0` 可关闭省略,quoted semantic number 同样兼容。
189189
- 本轮补充:Grep 文件列表输出现在支持 `files_without_match`/`filesWithoutMatch`/`files-without-match`/`--files-without-match`/`-L`,也接受 `output_mode``files_without_match(es)`,用于列出不含匹配的文件并兼容 quoted boolean。
190190
- 本轮补充:Grep 文件列表输出现在显式支持 `files_with_match(es)`/`filesWithMatch(es)`/`files-with-match(es)`/`--files-with-match(es)`/`-l`,并接受 `output_mode``files_with_match` alias,统一归一为 `files_with_matches`
191+
- 本轮补充:Grep 路径过滤现在支持 ripgrep 风格 `--glob`/`-g``--type`/`-t` aliases,执行和 structured content 都统一使用归一化后的 glob/type 过滤值。
191192
- 本轮补充:Grep 搜索现在支持 `no_ignore`/`noIgnore`/`no-ignore`/`--no-ignore`,可跳过 `.gitignore`/`.ignore` 规则,同时继续排除 VCS metadata 目录并保留 Read deny 额外 ignore 保护;`--no-ignore` 兼容 quoted boolean。
192193
- 本轮补充:Grep 的 `files_with_matches` 输出现在按官方行为使用文件修改时间倒序排序,mtime 相同再按路径排序;分页和 `head_limit` 会在排序后应用。
193194
- 本轮补充:Glob/Grep 搜索遍历现在会读取 permission context 中的 `Read(...)` deny 规则,并把对应 basename/path/directory pattern 作为额外 ignore rule,避免被禁止读取的文件出现在搜索结果中。

internal/tools/file/search_tools.go

Lines changed: 36 additions & 6 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": {}, "type": {}, "output_mode": {}, "outputMode": {}, "limit": {},
30+
"pattern": {}, "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": {}, "-n": {},
@@ -70,7 +70,11 @@ type grepInput struct {
7070
Pattern string `json:"pattern"`
7171
Path string `json:"path,omitempty"`
7272
Glob string `json:"glob,omitempty"`
73+
LongGlob string `json:"--glob,omitempty"`
74+
ShortGlob string `json:"-g,omitempty"`
7375
Type string `json:"type,omitempty"`
76+
LongType string `json:"--type,omitempty"`
77+
ShortType string `json:"-t,omitempty"`
7478
OutputMode string `json:"output_mode,omitempty"`
7579
OutputModeAlt string `json:"outputMode,omitempty"`
7680
Limit *int `json:"limit,omitempty"`
@@ -225,7 +229,11 @@ func NewGrepTool() tool.Tool {
225229
"pattern": map[string]any{"type": "string"},
226230
"path": map[string]any{"type": "string"},
227231
"glob": map[string]any{"type": "string"},
232+
"--glob": map[string]any{"type": "string"},
233+
"-g": map[string]any{"type": "string"},
228234
"type": map[string]any{"type": "string"},
235+
"--type": map[string]any{"type": "string"},
236+
"-t": map[string]any{"type": "string"},
229237
"output_mode": map[string]any{"type": "string", "enum": []any{"files_with_match", "files_with_matches", "files_without_match", "files_without_matches", "content", "count"}},
230238
"outputMode": map[string]any{"type": "string", "enum": []any{"files_with_match", "files_with_matches", "files_without_match", "files_without_matches", "content", "count"}},
231239
"limit": map[string]any{"type": "integer"},
@@ -330,7 +338,7 @@ func NewGrepTool() tool.Tool {
330338
},
331339
},
332340
PromptFunc: func(tool.PromptContext) (string, error) {
333-
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 and 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 control, offset, head_limit pagination, max_count/-m per-file match limiting, max_columns/--max-columns long-line omission, and only_matching/-o matched-text output. Use files_with_matches or -l to list files with matches, and files_without_match or -L to list files without matches. Count mode supports count_matches/--count-matches for occurrence counts. Use fixed_strings or -F for literal matching, word_regexp or -w for whole-word matches, and invert_match or -v 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
341+
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 control, offset, head_limit pagination, max_count/-m per-file match limiting, max_columns/--max-columns long-line omission, and only_matching/-o matched-text output. Use files_with_matches or -l to list files with matches, and files_without_match or -L to list files without matches. Count mode supports count_matches/--count-matches for occurrence counts. Use fixed_strings or -F for literal matching, word_regexp or -w for whole-word matches, and invert_match or -v 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
334342
},
335343
NormalizeFunc: normalizeGrepRawInput,
336344
ValidateFunc: validateGrep,
@@ -445,7 +453,7 @@ func validateGrep(ctx tool.Context, raw json.RawMessage) error {
445453
if input.LongMaxColumns != nil && *input.LongMaxColumns < 0 {
446454
return fmt.Errorf("max_columns must be non-negative")
447455
}
448-
if _, err := grepTypeExtensions(input.Type); err != nil {
456+
if _, err := grepTypeExtensions(grepTypeFilter(input)); err != nil {
449457
return err
450458
}
451459
before, after := grepContextLines(input)
@@ -500,7 +508,9 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
500508
CountMatches: countMatches,
501509
}
502510
noIgnore := grepNoIgnore(input)
503-
matches, totalMatches, truncated, err := collectGrepMatches(root, displayRoot, input.Glob, input.Type, expr, options, grepWalkOptions(ctx, root, noIgnore))
511+
globFilter := grepGlobFilter(input)
512+
typeFilter := grepTypeFilter(input)
513+
matches, totalMatches, truncated, err := collectGrepMatches(root, displayRoot, globFilter, typeFilter, expr, options, grepWalkOptions(ctx, root, noIgnore))
504514
if err != nil {
505515
return contracts.ToolResult{}, err
506516
}
@@ -514,8 +524,8 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
514524
"type": "grep",
515525
"pattern": input.Pattern,
516526
"path": input.Path,
517-
"glob": input.Glob,
518-
"type_filter": input.Type,
527+
"glob": globFilter,
528+
"type_filter": typeFilter,
519529
"output_mode": mode,
520530
"matches": grepStructuredMatches(matches, mode),
521531
"total_matches": totalMatches,
@@ -1186,6 +1196,26 @@ func compileGrepPattern(input grepInput) (*regexp.Regexp, error) {
11861196
return regexp.Compile(pattern)
11871197
}
11881198

1199+
func grepGlobFilter(input grepInput) string {
1200+
if strings.TrimSpace(input.Glob) != "" {
1201+
return input.Glob
1202+
}
1203+
if strings.TrimSpace(input.LongGlob) != "" {
1204+
return input.LongGlob
1205+
}
1206+
return input.ShortGlob
1207+
}
1208+
1209+
func grepTypeFilter(input grepInput) string {
1210+
if strings.TrimSpace(input.Type) != "" {
1211+
return input.Type
1212+
}
1213+
if strings.TrimSpace(input.LongType) != "" {
1214+
return input.LongType
1215+
}
1216+
return input.ShortType
1217+
}
1218+
11891219
func grepCaseInsensitive(input grepInput) bool {
11901220
return input.IgnoreCase || input.CaseInsensitive || input.CaseInsensitiveAlt || input.ShortIgnoreCase
11911221
}

internal/tools/file/tools_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,32 @@ func TestGrepToolOutputModesAndGlobFilter(t *testing.T) {
14121412
t.Fatalf("multi glob result = %#v", multiGlobResult.Content)
14131413
}
14141414

1415+
longGlobResult, err := executor.Execute(ctx, contracts.ToolUse{
1416+
ID: "toolu_grep_long_glob",
1417+
Name: "Grep",
1418+
Input: json.RawMessage(`{"pattern":"Alpha","--glob":"**/*.txt"}`),
1419+
}, nil)
1420+
if err != nil {
1421+
t.Fatal(err)
1422+
}
1423+
if longGlobResult.Content != "Found 1 file\nsrc/b.txt" ||
1424+
longGlobResult.StructuredContent["glob"] != "**/*.txt" {
1425+
t.Fatalf("long glob result = %#v", longGlobResult)
1426+
}
1427+
1428+
shortGlobResult, err := executor.Execute(ctx, contracts.ToolUse{
1429+
ID: "toolu_grep_short_glob",
1430+
Name: "Grep",
1431+
Input: json.RawMessage(`{"pattern":"Alpha","-g":"**/*.txt"}`),
1432+
}, nil)
1433+
if err != nil {
1434+
t.Fatal(err)
1435+
}
1436+
if shortGlobResult.Content != "Found 1 file\nsrc/b.txt" ||
1437+
shortGlobResult.StructuredContent["glob"] != "**/*.txt" {
1438+
t.Fatalf("short glob result = %#v", shortGlobResult)
1439+
}
1440+
14151441
braceGlobResult, err := executor.Execute(ctx, contracts.ToolUse{
14161442
ID: "toolu_grep_brace_glob",
14171443
Name: "Grep",
@@ -2117,6 +2143,30 @@ func TestGrepToolTypeFilter(t *testing.T) {
21172143
t.Fatalf("javascript type result = %#v", jsResult.Content)
21182144
}
21192145

2146+
longTypeResult, err := executor.Execute(ctx, contracts.ToolUse{
2147+
ID: "toolu_grep_long_type",
2148+
Name: "Grep",
2149+
Input: json.RawMessage(`{"pattern":"Needle","--type":"javascript"}`),
2150+
}, nil)
2151+
if err != nil {
2152+
t.Fatal(err)
2153+
}
2154+
if longTypeResult.Content != "Found 1 file\nsrc/c.jsx" || longTypeResult.StructuredContent["type_filter"] != "javascript" {
2155+
t.Fatalf("long type result = %#v", longTypeResult)
2156+
}
2157+
2158+
shortTypeResult, err := executor.Execute(ctx, contracts.ToolUse{
2159+
ID: "toolu_grep_short_type",
2160+
Name: "Grep",
2161+
Input: json.RawMessage(`{"pattern":"Needle","-t":"go"}`),
2162+
}, nil)
2163+
if err != nil {
2164+
t.Fatal(err)
2165+
}
2166+
if shortTypeResult.Content != "Found 1 file\nsrc/a.go" || shortTypeResult.StructuredContent["type_filter"] != "go" {
2167+
t.Fatalf("short type result = %#v", shortTypeResult)
2168+
}
2169+
21202170
_, err = executor.Execute(ctx, contracts.ToolUse{
21212171
ID: "toolu_grep_bad_type",
21222172
Name: "Grep",

0 commit comments

Comments
 (0)