Skip to content

Commit fe29c35

Browse files
author
SqlRush
committed
Add Grep filename output controls
1 parent 39c84af commit fe29c35

4 files changed

Lines changed: 152 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ test/parity/ # golden tests against TS/official behavior
213213
- 本轮补充:Grep 长行输出现在支持 ripgrep 风格 `max_columns_preview`/`--max-columns-preview``no_max_columns_preview`/`--no-max-columns-preview`,会在 `max_columns` 触发时输出截断预览加官方 omitted-end 后缀;quoted semantic boolean 同样兼容。
214214
- 本轮补充:Grep 搜索现在支持 ripgrep 风格 `line_regexp`/`line-regexp`/`--line-regexp`/`-x`,将 pattern 限定为整行匹配,并按官方语义优先于 `word_regexp`;fixed-string、multiline 和 quoted semantic boolean 组合均兼容。
215215
- 本轮补充:Grep content 输出现在支持 ripgrep 风格 `vimgrep`/`--vimgrep`,匹配行按每个匹配重复输出 `path:line:column:text`,context 行保持单行输出,并兼容 `-N``only_matching` 和 quoted semantic boolean。
216+
- 本轮补充:Grep content/count 输出现在支持 ripgrep 风格 `with_filename`/`--with-filename`/`-H``no_filename`/`--no-filename`/`-I`,可控制匹配行和计数输出的文件名前缀;文件列表模式仍保留路径输出。
216217
- 本轮补充:Grep 常用布尔参数继续补齐 ripgrep 长参数 aliases,覆盖 `--line-number``--ignore-case``--fixed-strings``--word-regexp``--invert-match``--only-matching`,并兼容 quoted semantic boolean。
217218
- 本轮补充:Grep multiline 搜索现在支持 ripgrep 风格 `-U``--multiline``multiline-dotall``--multiline-dotall` aliases,统一映射到既有跨行 dotall 匹配逻辑并兼容 quoted semantic boolean。
218219
- 本轮补充:Grep 搜索现在支持 `no_ignore`/`noIgnore`/`no-ignore`/`--no-ignore`,可跳过 `.gitignore`/`.ignore` 规则,同时继续排除 VCS metadata 目录并保留 Read deny 额外 ignore 保护;`--no-ignore` 兼容 quoted boolean。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Covered behavior:
138138
- `Grep` now accepts `line_regexp`/`line-regexp`/`--line-regexp`/`-x` to require whole-line matches, with fixed-string and word-regexp precedence covered.
139139
- `Grep` content output now accepts `passthru`/`passthrough`/`--passthru`/`--passthrough`, printing all searched lines while preserving matched-line markers and overriding context counts.
140140
- `Grep` content output now accepts `vimgrep`/`--vimgrep`, repeating matching lines once per match with column metadata while preserving context-line output and `-N` formatting.
141+
- `Grep` content and count output now accept `with_filename`/`--with-filename`/`-H` plus `no_filename`/`--no-filename`/`-I`, while file-list modes continue to emit paths.
141142
- `Grep` content output now accepts `trim`/`--trim` plus `no_trim`/`--no-trim`, trimming leading ASCII whitespace from printed line text while preserving original match columns.
142143
- `Grep` long-line output now accepts `max_columns_preview`/`--max-columns-preview` plus `no_max_columns_preview`/`--no-max-columns-preview`, showing a ripgrep-style truncated preview when `max_columns` is exceeded.
143144
- `Glob`/`Grep` traversal now applies `Read(...)` deny rules from the permission context as extra search ignore rules, hiding denied basename, path, and directory patterns from search results.

internal/tools/file/search_tools.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var allowedGrepInputKeys = map[string]struct{}{
3333
"head_limit": {}, "headLimit": {}, "offset": {}, "max_count": {}, "maxCount": {}, "-m": {},
3434
"max_columns": {}, "maxColumns": {}, "max-columns": {}, "--max-columns": {},
3535
"max_columns_preview": {}, "maxColumnsPreview": {}, "max-columns-preview": {}, "--max-columns-preview": {}, "no_max_columns_preview": {}, "noMaxColumnsPreview": {}, "no-max-columns-preview": {}, "--no-max-columns-preview": {},
36+
"with_filename": {}, "withFilename": {}, "with-filename": {}, "--with-filename": {}, "-H": {}, "no_filename": {}, "noFilename": {}, "no-filename": {}, "--no-filename": {}, "-I": {},
3637
"sort": {}, "--sort": {}, "sortr": {}, "--sortr": {},
3738
"context": {}, "-C": {}, "before_context": {}, "beforeContext": {}, "-B": {}, "after_context": {}, "afterContext": {}, "-A": {}, "line_numbers": {}, "lineNumbers": {}, "line-number": {}, "--line-number": {}, "-n": {},
3839
"no_line_number": {}, "noLineNumber": {}, "no_line_numbers": {}, "noLineNumbers": {}, "no-line-number": {}, "no-line-numbers": {}, "--no-line-number": {}, "-N": {},
@@ -64,6 +65,7 @@ var grepSemanticNumberKeys = map[string]struct{}{
6465

6566
var grepSemanticBooleanKeys = map[string]struct{}{
6667
"max_columns_preview": {}, "maxColumnsPreview": {}, "max-columns-preview": {}, "--max-columns-preview": {}, "no_max_columns_preview": {}, "noMaxColumnsPreview": {}, "no-max-columns-preview": {}, "--no-max-columns-preview": {},
68+
"with_filename": {}, "withFilename": {}, "with-filename": {}, "--with-filename": {}, "-H": {}, "no_filename": {}, "noFilename": {}, "no-filename": {}, "--no-filename": {}, "-I": {},
6769
"line_numbers": {}, "lineNumbers": {}, "line-number": {}, "--line-number": {}, "-n": {},
6870
"no_line_number": {}, "noLineNumber": {}, "no_line_numbers": {}, "noLineNumbers": {}, "no-line-number": {}, "no-line-numbers": {}, "--no-line-number": {}, "-N": {},
6971
"column": {}, "column_numbers": {}, "columnNumbers": {}, "column-number": {}, "--column": {},
@@ -125,6 +127,16 @@ type grepInput struct {
125127
NoMaxColumnsPreviewAlt bool `json:"noMaxColumnsPreview,omitempty"`
126128
NoMaxColumnsPreviewDash bool `json:"no-max-columns-preview,omitempty"`
127129
LongNoMaxColumnsPreview bool `json:"--no-max-columns-preview,omitempty"`
130+
WithFilename bool `json:"with_filename,omitempty"`
131+
WithFilenameAlt bool `json:"withFilename,omitempty"`
132+
WithFilenameDash bool `json:"with-filename,omitempty"`
133+
LongWithFilename bool `json:"--with-filename,omitempty"`
134+
ShortWithFilename bool `json:"-H,omitempty"`
135+
NoFilename bool `json:"no_filename,omitempty"`
136+
NoFilenameAlt bool `json:"noFilename,omitempty"`
137+
NoFilenameDash bool `json:"no-filename,omitempty"`
138+
LongNoFilename bool `json:"--no-filename,omitempty"`
139+
ShortNoFilename bool `json:"-I,omitempty"`
128140
Sort string `json:"sort,omitempty"`
129141
LongSort string `json:"--sort,omitempty"`
130142
SortReverse string `json:"sortr,omitempty"`
@@ -270,6 +282,7 @@ type grepOptions struct {
270282
MaxCount int
271283
MaxColumns int
272284
MaxPreview bool
285+
WithFilename bool
273286
BeforeContext int
274287
AfterContext int
275288
LineNumbers bool
@@ -371,9 +384,19 @@ func NewGrepTool() tool.Tool {
371384
"--no-max-columns-preview": map[string]any{
372385
"type": "boolean",
373386
},
374-
"sort": map[string]any{"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"}},
375-
"--sort": map[string]any{"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"}},
376-
"sortr": map[string]any{"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"}},
387+
"with_filename": map[string]any{"type": "boolean"},
388+
"withFilename": map[string]any{"type": "boolean"},
389+
"with-filename": map[string]any{"type": "boolean"},
390+
"--with-filename": map[string]any{"type": "boolean"},
391+
"-H": map[string]any{"type": "boolean"},
392+
"no_filename": map[string]any{"type": "boolean"},
393+
"noFilename": map[string]any{"type": "boolean"},
394+
"no-filename": map[string]any{"type": "boolean"},
395+
"--no-filename": map[string]any{"type": "boolean"},
396+
"-I": map[string]any{"type": "boolean"},
397+
"sort": map[string]any{"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"}},
398+
"--sort": map[string]any{"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"}},
399+
"sortr": map[string]any{"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"}},
377400
"--sortr": map[string]any{
378401
"type": "string", "enum": []any{"path", "name", "file", "modified", "mtime", "modtime", "time", "none"},
379402
},
@@ -529,7 +552,7 @@ func NewGrepTool() tool.Tool {
529552
},
530553
},
531554
PromptFunc: func(tool.PromptContext) (string, error) {
532-
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 and -N/--no-line-number line-number control, --column column-number output, offset, head_limit pagination, max_count/-m per-file match limiting, max_columns/--max-columns long-line omission, --max-columns-preview long-line previews, only_matching/-o/--only-matching matched-text output, vimgrep/--vimgrep per-match line output, passthru/--passthru/--passthrough all-line output, and trim/--trim leading-whitespace trimming. 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 sort/--sort or sortr/--sortr with path or modified to control result ordering. Use fixed_strings/-F/--fixed-strings for literal matching, text/-a/--text to search binary-extension files as text, word_regexp/-w/--word-regexp for whole-word matches, line_regexp/-x/--line-regexp for whole-line matches, ignore_case/-i/--ignore-case for case-insensitive search, case_sensitive/-s/--case-sensitive to force case-sensitive matching, smart_case/-S/--smart-case for lowercase-only patterns, 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
555+
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 and -N/--no-line-number line-number control, --column column-number output, -H/--with-filename and -I/--no-filename filename prefix control, offset, head_limit pagination, max_count/-m per-file match limiting, max_columns/--max-columns long-line omission, --max-columns-preview long-line previews, only_matching/-o/--only-matching matched-text output, vimgrep/--vimgrep per-match line output, passthru/--passthru/--passthrough all-line output, and trim/--trim leading-whitespace trimming. 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 sort/--sort or sortr/--sortr with path or modified to control result ordering. Use fixed_strings/-F/--fixed-strings for literal matching, text/-a/--text to search binary-extension files as text, word_regexp/-w/--word-regexp for whole-word matches, line_regexp/-x/--line-regexp for whole-line matches, ignore_case/-i/--ignore-case for case-insensitive search, case_sensitive/-s/--case-sensitive to force case-sensitive matching, smart_case/-S/--smart-case for lowercase-only patterns, 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
533556
},
534557
NormalizeFunc: normalizeGrepRawInput,
535558
ValidateFunc: validateGrep,
@@ -706,6 +729,7 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
706729
MaxCount: grepMaxCount(input),
707730
MaxColumns: grepMaxColumns(input),
708731
MaxPreview: grepMaxColumnsPreview(input),
732+
WithFilename: grepWithFilename(input, mode),
709733
BeforeContext: before,
710734
AfterContext: after,
711735
LineNumbers: grepLineNumbers(input, mode),
@@ -749,6 +773,8 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
749773
"max_count": options.MaxCount,
750774
"max_columns": options.MaxColumns,
751775
"max_columns_preview": options.MaxPreview,
776+
"with_filename": options.WithFilename,
777+
"no_filename": !options.WithFilename && (mode == "content" || mode == "count"),
752778
"before_context": options.BeforeContext,
753779
"after_context": options.AfterContext,
754780
"line_numbers": options.LineNumbers,
@@ -1316,24 +1342,42 @@ func formatGrepMatches(matches []grepMatch, options grepOptions) string {
13161342
case "files_with_matches", "files_without_matches":
13171343
lines = append(lines, match.Path)
13181344
case "count":
1319-
lines = append(lines, fmt.Sprintf("%s:%d", match.Path, match.Count))
1345+
if options.WithFilename {
1346+
lines = append(lines, fmt.Sprintf("%s:%d", match.Path, match.Count))
1347+
} else {
1348+
lines = append(lines, fmt.Sprintf("%d", match.Count))
1349+
}
13201350
default:
13211351
separator := ":"
13221352
if !match.Matched {
13231353
separator = "-"
13241354
}
1355+
if options.WithFilename {
1356+
if options.LineNumbers {
1357+
if (options.ColumnNumbers || options.Vimgrep) && match.Matched && match.Column > 0 {
1358+
lines = append(lines, fmt.Sprintf("%s%s%d%s%d%s%s", match.Path, separator, match.Line, separator, match.Column, separator, match.Text))
1359+
continue
1360+
}
1361+
lines = append(lines, fmt.Sprintf("%s%s%d%s%s", match.Path, separator, match.Line, separator, match.Text))
1362+
} else {
1363+
if options.Vimgrep && match.Matched && match.Column > 0 {
1364+
lines = append(lines, fmt.Sprintf("%s%s%d%s%s", match.Path, separator, match.Column, separator, match.Text))
1365+
continue
1366+
}
1367+
lines = append(lines, fmt.Sprintf("%s%s%s", match.Path, separator, match.Text))
1368+
}
1369+
continue
1370+
}
13251371
if options.LineNumbers {
13261372
if (options.ColumnNumbers || options.Vimgrep) && match.Matched && match.Column > 0 {
1327-
lines = append(lines, fmt.Sprintf("%s%s%d%s%d%s%s", match.Path, separator, match.Line, separator, match.Column, separator, match.Text))
1373+
lines = append(lines, fmt.Sprintf("%d%s%d%s%s", match.Line, separator, match.Column, separator, match.Text))
13281374
continue
13291375
}
1330-
lines = append(lines, fmt.Sprintf("%s%s%d%s%s", match.Path, separator, match.Line, separator, match.Text))
1376+
lines = append(lines, fmt.Sprintf("%d%s%s", match.Line, separator, match.Text))
1377+
} else if options.Vimgrep && match.Matched && match.Column > 0 {
1378+
lines = append(lines, fmt.Sprintf("%d%s%s", match.Column, separator, match.Text))
13311379
} else {
1332-
if options.Vimgrep && match.Matched && match.Column > 0 {
1333-
lines = append(lines, fmt.Sprintf("%s%s%d%s%s", match.Path, separator, match.Column, separator, match.Text))
1334-
continue
1335-
}
1336-
lines = append(lines, fmt.Sprintf("%s%s%s", match.Path, separator, match.Text))
1380+
lines = append(lines, match.Text)
13371381
}
13381382
}
13391383
}
@@ -2002,6 +2046,20 @@ func grepMaxColumnsPreview(input grepInput) bool {
20022046
input.LongMaxColumnsPreview
20032047
}
20042048

2049+
func grepWithFilename(input grepInput, mode string) bool {
2050+
if mode != "content" && mode != "count" {
2051+
return true
2052+
}
2053+
if input.NoFilename ||
2054+
input.NoFilenameAlt ||
2055+
input.NoFilenameDash ||
2056+
input.LongNoFilename ||
2057+
input.ShortNoFilename {
2058+
return false
2059+
}
2060+
return true
2061+
}
2062+
20052063
func grepContextLines(input grepInput) (int, int) {
20062064
if input.Context != nil {
20072065
return *input.Context, *input.Context

internal/tools/file/tools_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,6 +2009,86 @@ func TestGrepToolVimgrep(t *testing.T) {
20092009
}
20102010
}
20112011

2012+
func TestGrepToolFilenameControls(t *testing.T) {
2013+
dir := t.TempDir()
2014+
if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("before\nNeedle Needle\nafter\n"), 0o644); err != nil {
2015+
t.Fatal(err)
2016+
}
2017+
if err := os.WriteFile(filepath.Join(dir, "b.txt"), []byte("Needle\n"), 0o644); err != nil {
2018+
t.Fatal(err)
2019+
}
2020+
executor := fileExecutor(t)
2021+
ctx := fileToolContext(dir)
2022+
2023+
contentResult, err := executor.Execute(ctx, contracts.ToolUse{
2024+
ID: "toolu_grep_no_filename_content",
2025+
Name: "Grep",
2026+
Input: json.RawMessage(`{"pattern":"Needle","glob":"a.txt","output_mode":"content","--no-filename":"true","context":1}`),
2027+
}, nil)
2028+
if err != nil {
2029+
t.Fatal(err)
2030+
}
2031+
wantContent := "1-before\n2:Needle Needle\n3-after"
2032+
if contentResult.Content != wantContent ||
2033+
contentResult.StructuredContent["with_filename"] != false ||
2034+
contentResult.StructuredContent["no_filename"] != true {
2035+
t.Fatalf("no-filename content result = %#v", contentResult)
2036+
}
2037+
2038+
countResult, err := executor.Execute(ctx, contracts.ToolUse{
2039+
ID: "toolu_grep_no_filename_count",
2040+
Name: "Grep",
2041+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"count","no_filename":true}`),
2042+
}, nil)
2043+
if err != nil {
2044+
t.Fatal(err)
2045+
}
2046+
wantCount := "1\n1\n\nFound 2 total occurrences across 2 files."
2047+
if countResult.Content != wantCount || countResult.StructuredContent["with_filename"] != false {
2048+
t.Fatalf("no-filename count result = %#v", countResult)
2049+
}
2050+
2051+
vimgrepResult, err := executor.Execute(ctx, contracts.ToolUse{
2052+
ID: "toolu_grep_no_filename_vimgrep",
2053+
Name: "Grep",
2054+
Input: json.RawMessage(`{"pattern":"Needle","glob":"a.txt","output_mode":"content","-I":true,"-N":true,"--vimgrep":true}`),
2055+
}, nil)
2056+
if err != nil {
2057+
t.Fatal(err)
2058+
}
2059+
wantVimgrep := "1:Needle Needle\n8:Needle Needle"
2060+
if vimgrepResult.Content != wantVimgrep ||
2061+
vimgrepResult.StructuredContent["with_filename"] != false ||
2062+
vimgrepResult.StructuredContent["line_numbers"] != false {
2063+
t.Fatalf("no-filename vimgrep result = %#v", vimgrepResult)
2064+
}
2065+
2066+
filesResult, err := executor.Execute(ctx, contracts.ToolUse{
2067+
ID: "toolu_grep_no_filename_files",
2068+
Name: "Grep",
2069+
Input: json.RawMessage(`{"pattern":"Needle","--files-with-matches":true,"--no-filename":true,"sort":"path"}`),
2070+
}, nil)
2071+
if err != nil {
2072+
t.Fatal(err)
2073+
}
2074+
if filesResult.Content != "Found 2 files\na.txt\nb.txt" || filesResult.StructuredContent["with_filename"] != true {
2075+
t.Fatalf("no-filename files result = %#v", filesResult)
2076+
}
2077+
2078+
withResult, err := executor.Execute(ctx, contracts.ToolUse{
2079+
ID: "toolu_grep_with_filename",
2080+
Name: "Grep",
2081+
Input: json.RawMessage(`{"pattern":"Needle","glob":"a.txt","output_mode":"content","-H":true,"head_limit":1}`),
2082+
}, nil)
2083+
if err != nil {
2084+
t.Fatal(err)
2085+
}
2086+
wantWith := "a.txt:2:Needle Needle"
2087+
if withResult.Content != wantWith || withResult.StructuredContent["with_filename"] != true {
2088+
t.Fatalf("with-filename result = %#v", withResult)
2089+
}
2090+
}
2091+
20122092
func TestGrepToolTrim(t *testing.T) {
20132093
dir := t.TempDir()
20142094
content := strings.Join([]string{

0 commit comments

Comments
 (0)