Skip to content

Commit c3e3e9e

Browse files
author
SqlRush
committed
Add Grep quiet output control
1 parent 16fc0df commit c3e3e9e

4 files changed

Lines changed: 71 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ test/parity/ # golden tests against TS/official behavior
221221
- 本轮补充:Grep content 输出现在支持 ripgrep 风格 `passthru`/`passthrough`/`--passthru`/`--passthrough`,可输出被搜索文件的全部行并保留匹配行标记;启用后按官方语义覆盖 context 行数,quoted semantic boolean 同样兼容。
222222
- 本轮补充:Grep content 输出现在支持 ripgrep 风格 `trim`/`--trim``no_trim`/`--no-trim`,会删除每条已打印文本行开头的 ASCII 空白,同时保留原始匹配列号;quoted semantic boolean 同样兼容。
223223
- 本轮补充:Grep 输出现在支持 ripgrep 风格 `stats`/`--stats``no_stats`/`--no-stats`,在 content/count/匹配文件列表结果尾部追加 matches、matched lines、files searched、bytes searched/printed 和耗时统计,并在 structured content 暴露同源统计字段;JSON 输出模式仍未宣称完成。
224+
- 本轮补充:Grep 输出现在支持 ripgrep 风格 `quiet`/`--quiet`/`-q``no_quiet`/`--no-quiet`,可抑制文本输出但保留结构化搜索结果和总数;quiet 会关闭 stats 文本输出,quoted semantic boolean 同样兼容。
224225
- 本轮补充:Grep 长行输出现在支持 ripgrep 风格 `max_columns_preview`/`--max-columns-preview``no_max_columns_preview`/`--no-max-columns-preview`,会在 `max_columns` 触发时输出截断预览加官方 omitted-end 后缀;quoted semantic boolean 同样兼容。
225226
- 本轮补充:Grep 搜索现在支持 ripgrep 风格 `line_regexp`/`line-regexp`/`--line-regexp`/`-x`,将 pattern 限定为整行匹配,并按官方语义优先于 `word_regexp`;fixed-string、multiline 和 quoted semantic boolean 组合均兼容。
226227
- 本轮补充:Grep content 输出现在支持 ripgrep 风格 `vimgrep`/`--vimgrep`,匹配行按每个匹配重复输出 `path:line:column:text`,context 行保持单行输出,并兼容 `-N``only_matching` 和 quoted semantic boolean。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Covered behavior:
158158
- `Grep` traversal now accepts ripgrep-style `follow`/`--follow`/`-L` plus `no_follow`/`--no-follow`, skipping symlinks by default and following symlink files/directories with real-path loop protection when requested.
159159
- `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.
160160
- `Grep` output now accepts `stats`/`--stats` plus `no_stats`/`--no-stats`, appending ripgrep-style aggregate statistics for matches, matched lines, searched files, searched/printed bytes, and durations while exposing the same counters in structured results; full `--json` output remains a separate gap.
161+
- `Grep` output now accepts `quiet`/`--quiet`/`-q` plus `no_quiet`/`--no-quiet`, suppressing textual output while preserving structured matches and counters; quiet suppresses stats text output.
161162
- `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.
162163
- `Grep` file filtering now accepts ripgrep-style `max_filesize`/`--max-filesize` with optional `K`/`M`/`G` suffixes, skipping files larger than the parsed byte limit before content reads or file-list output.
163164
- `Grep` search now accepts ripgrep-style `crlf`/`--crlf` plus `no_crlf`/`--no-crlf`, preserving default CRLF anchor behavior while allowing CRLF-aware anchors with original byte offsets.

internal/tools/file/search_tools.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ var allowedGrepInputKeys = map[string]struct{}{
7272
"passthru": {}, "passthrough": {}, "--passthru": {}, "--passthrough": {},
7373
"trim": {}, "--trim": {}, "no_trim": {}, "noTrim": {}, "no-trim": {}, "--no-trim": {},
7474
"stats": {}, "--stats": {}, "no_stats": {}, "noStats": {}, "no-stats": {}, "--no-stats": {},
75+
"quiet": {}, "--quiet": {}, "-q": {}, "no_quiet": {}, "noQuiet": {}, "no-quiet": {}, "--no-quiet": {},
7576
"files": {}, "--files": {},
7677
"files_with_match": {}, "filesWithMatch": {}, "files-with-match": {}, "--files-with-match": {}, "files_with_matches": {}, "filesWithMatches": {}, "files-with-matches": {}, "--files-with-matches": {}, "-l": {},
7778
"files_without_match": {}, "filesWithoutMatch": {}, "files-without-match": {}, "--files-without-match": {}, "files_without_matches": {}, "filesWithoutMatches": {}, "files-without-matches": {}, "--files-without-matches": {},
@@ -122,6 +123,7 @@ var grepSemanticBooleanKeys = map[string]struct{}{
122123
"passthru": {}, "passthrough": {}, "--passthru": {}, "--passthrough": {},
123124
"trim": {}, "--trim": {}, "no_trim": {}, "noTrim": {}, "no-trim": {}, "--no-trim": {},
124125
"stats": {}, "--stats": {}, "no_stats": {}, "noStats": {}, "no-stats": {}, "--no-stats": {},
126+
"quiet": {}, "--quiet": {}, "-q": {}, "no_quiet": {}, "noQuiet": {}, "no-quiet": {}, "--no-quiet": {},
125127
"files": {}, "--files": {},
126128
"files_with_match": {}, "filesWithMatch": {}, "files-with-match": {}, "--files-with-match": {}, "files_with_matches": {}, "filesWithMatches": {}, "files-with-matches": {}, "--files-with-matches": {}, "-l": {},
127129
"files_without_match": {}, "filesWithoutMatch": {}, "files-without-match": {}, "--files-without-match": {}, "files_without_matches": {}, "filesWithoutMatches": {}, "files-without-matches": {}, "--files-without-matches": {},
@@ -378,6 +380,13 @@ type grepInput struct {
378380
NoStatsAlt bool `json:"noStats,omitempty"`
379381
NoStatsDash bool `json:"no-stats,omitempty"`
380382
LongNoStats bool `json:"--no-stats,omitempty"`
383+
Quiet bool `json:"quiet,omitempty"`
384+
LongQuiet bool `json:"--quiet,omitempty"`
385+
ShortQuiet bool `json:"-q,omitempty"`
386+
NoQuiet bool `json:"no_quiet,omitempty"`
387+
NoQuietAlt bool `json:"noQuiet,omitempty"`
388+
NoQuietDash bool `json:"no-quiet,omitempty"`
389+
LongNoQuiet bool `json:"--no-quiet,omitempty"`
381390
Files bool `json:"files,omitempty"`
382391
LongFiles bool `json:"--files,omitempty"`
383392
FilesWithMatch bool `json:"files_with_match,omitempty"`
@@ -514,6 +523,7 @@ type grepOptions struct {
514523
Passthru bool
515524
Trim bool
516525
Stats bool
526+
Quiet bool
517527
CountMatches bool
518528
IncludeZero bool
519529
ColumnNumbers bool
@@ -835,6 +845,13 @@ func NewGrepTool() tool.Tool {
835845
"noStats": map[string]any{"type": "boolean"},
836846
"no-stats": map[string]any{"type": "boolean"},
837847
"--no-stats": map[string]any{"type": "boolean"},
848+
"quiet": map[string]any{"type": "boolean"},
849+
"--quiet": map[string]any{"type": "boolean"},
850+
"-q": map[string]any{"type": "boolean"},
851+
"no_quiet": map[string]any{"type": "boolean"},
852+
"noQuiet": map[string]any{"type": "boolean"},
853+
"no-quiet": map[string]any{"type": "boolean"},
854+
"--no-quiet": map[string]any{"type": "boolean"},
838855
"files": map[string]any{"type": "boolean"},
839856
"--files": map[string]any{"type": "boolean"},
840857
"files_with_match": map[string]any{
@@ -947,7 +964,7 @@ func NewGrepTool() tool.Tool {
947964
},
948965
},
949966
PromptFunc: func(tool.PromptContext) (string, error) {
950-
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, and pattern_file/--file/-f can read one pattern per line from a file. output_mode may be files, files_with_matches, files_without_matches, content, or count; glob/-g/--glob, iglob/--iglob, type/-t/--type, and type_not/-T/--type-not optionally filter file paths. glob and iglob accept whitespace/comma-separated patterns, negation, and brace alternation; glob_case_insensitive/--glob-case-insensitive makes glob patterns ignore case. 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, byte_offset/--byte-offset/-b byte offset output, -H/--with-filename and -I/--no-filename filename prefix control, heading/--heading grouped file headings, path_separator/--path-separator display path separator control, null/--null NUL path terminators/separators, field_match_separator/--field-match-separator and field_context_separator/--field-context-separator output field separators, context_separator/--context-separator and no_context_separator/--no-context-separator context group separator 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, replace/--replace/-r display-only replacement, only_matching/-o/--only-matching matched-text output, vimgrep/--vimgrep per-match line output, passthru/--passthru/--passthrough all-line output, trim/--trim leading-whitespace trimming, stats/--stats aggregate statistics, and hidden/--hidden or no_hidden/--no-hidden hidden file traversal control. Use files/--files to list files that would be searched without requiring pattern, files_with_matches or -l to list files with matches, files_without_match to list files without matches, and count/--count/-c for count mode. Count mode supports count_matches/--count-matches for occurrence counts and include_zero/--include-zero to include zero-count files. Use max_depth/--max-depth/-d to limit directory descent, max_filesize/--max-filesize with optional K/M/G suffix to skip larger files, follow/--follow/-L or no_follow/--no-follow to control symlink traversal, and sort/--sort or sortr/--sortr with path or modified to control result ordering; --sort-files is accepted as a path-sort alias. Use fixed_strings/-F/--fixed-strings for literal matching, encoding/--encoding/-E to choose auto/none/utf-8/utf-16/utf-16le/utf-16be text decoding, null_data/--null-data to use NUL as the input line terminator, crlf/--crlf to treat CRLF/CR/LF as line terminators for anchors, text/-a/--text to search binary-extension files as text, no_text/--no-text to disable text mode, 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/.rgignore files, no_ignore_dot/--no-ignore-dot to skip .ignore/.rgignore while keeping .gitignore active, no_ignore_vcs/--no-ignore-vcs to skip .gitignore while keeping .ignore/.rgignore active, ignore_file/--ignore-file to add a gitignore-formatted file matched relative to the current working directory, or no_ignore_files/--no-ignore-files to ignore explicit ignore_file inputs; VCS metadata and read-denied paths remain excluded. Set multiline to allow patterns to span lines with dot matching newlines.", nil
967+
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, and pattern_file/--file/-f can read one pattern per line from a file. output_mode may be files, files_with_matches, files_without_matches, content, or count; glob/-g/--glob, iglob/--iglob, type/-t/--type, and type_not/-T/--type-not optionally filter file paths. glob and iglob accept whitespace/comma-separated patterns, negation, and brace alternation; glob_case_insensitive/--glob-case-insensitive makes glob patterns ignore case. 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, byte_offset/--byte-offset/-b byte offset output, -H/--with-filename and -I/--no-filename filename prefix control, heading/--heading grouped file headings, path_separator/--path-separator display path separator control, null/--null NUL path terminators/separators, field_match_separator/--field-match-separator and field_context_separator/--field-context-separator output field separators, context_separator/--context-separator and no_context_separator/--no-context-separator context group separator 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, replace/--replace/-r display-only replacement, only_matching/-o/--only-matching matched-text output, vimgrep/--vimgrep per-match line output, passthru/--passthru/--passthrough all-line output, trim/--trim leading-whitespace trimming, stats/--stats aggregate statistics, quiet/--quiet/-q output suppression, and hidden/--hidden or no_hidden/--no-hidden hidden file traversal control. Use files/--files to list files that would be searched without requiring pattern, files_with_matches or -l to list files with matches, files_without_match to list files without matches, and count/--count/-c for count mode. Count mode supports count_matches/--count-matches for occurrence counts and include_zero/--include-zero to include zero-count files. Use max_depth/--max-depth/-d to limit directory descent, max_filesize/--max-filesize with optional K/M/G suffix to skip larger files, follow/--follow/-L or no_follow/--no-follow to control symlink traversal, and sort/--sort or sortr/--sortr with path or modified to control result ordering; --sort-files is accepted as a path-sort alias. Use fixed_strings/-F/--fixed-strings for literal matching, encoding/--encoding/-E to choose auto/none/utf-8/utf-16/utf-16le/utf-16be text decoding, null_data/--null-data to use NUL as the input line terminator, crlf/--crlf to treat CRLF/CR/LF as line terminators for anchors, text/-a/--text to search binary-extension files as text, no_text/--no-text to disable text mode, 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/.rgignore files, no_ignore_dot/--no-ignore-dot to skip .ignore/.rgignore while keeping .gitignore active, no_ignore_vcs/--no-ignore-vcs to skip .gitignore while keeping .ignore/.rgignore active, ignore_file/--ignore-file to add a gitignore-formatted file matched relative to the current working directory, or no_ignore_files/--no-ignore-files to ignore explicit ignore_file inputs; VCS metadata and read-denied paths remain excluded. Set multiline to allow patterns to span lines with dot matching newlines.", nil
951968
},
952969
NormalizeFunc: normalizeGrepRawInput,
953970
ValidateFunc: validateGrep,
@@ -1147,7 +1164,8 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
11471164
after = 0
11481165
}
11491166
trim := grepTrim(input) && mode == "content"
1150-
statsEnabled := grepStatsEnabled(input) && mode != "files"
1167+
quiet := grepQuiet(input)
1168+
statsEnabled := grepStatsEnabled(input) && mode != "files" && !quiet
11511169
heading := grepHeading(input) && mode == "content" && !vimgrep
11521170
pathSeparator := grepPathSeparator(input)
11531171
null := grepNull(input)
@@ -1209,6 +1227,7 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
12091227
Passthru: passthru,
12101228
Trim: trim,
12111229
Stats: statsEnabled,
1230+
Quiet: quiet,
12121231
CountMatches: countMatches,
12131232
IncludeZero: includeZero,
12141233
ColumnNumbers: grepColumnNumbers(input),
@@ -1239,7 +1258,9 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
12391258
stats.SearchDuration = time.Since(started)
12401259
content := formatGrepResultContent(matches, options, truncated, &stats)
12411260
stats.TotalDuration = time.Since(started)
1242-
if content == "" {
1261+
if options.Quiet {
1262+
content = ""
1263+
} else if content == "" {
12431264
content = "No matches found"
12441265
}
12451266
if options.Stats {
@@ -1303,6 +1324,8 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
13031324
"vimgrep": vimgrep,
13041325
"passthru": passthru,
13051326
"trim": trim,
1327+
"quiet": quiet,
1328+
"no_quiet": grepNoQuiet(input),
13061329
"stats_enabled": statsEnabled,
13071330
"stats": grepStructuredStats(stats, statsEnabled),
13081331
"files": mode == "files",
@@ -2942,6 +2965,20 @@ func grepStatsEnabled(input grepInput) bool {
29422965
return input.Stats || input.LongStats
29432966
}
29442967

2968+
func grepQuiet(input grepInput) bool {
2969+
if grepNoQuiet(input) {
2970+
return false
2971+
}
2972+
return input.Quiet || input.LongQuiet || input.ShortQuiet
2973+
}
2974+
2975+
func grepNoQuiet(input grepInput) bool {
2976+
return input.NoQuiet ||
2977+
input.NoQuietAlt ||
2978+
input.NoQuietDash ||
2979+
input.LongNoQuiet
2980+
}
2981+
29452982
func grepHeading(input grepInput) bool {
29462983
if input.NoHeading || input.NoHeadingAlt || input.NoHeadingDash || input.LongNoHeading {
29472984
return false

internal/tools/file/tools_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,35 @@ func TestGrepToolStats(t *testing.T) {
16141614
noStatsResult.StructuredContent["stats"] != nil {
16151615
t.Fatalf("no-stats override result = %#v", noStatsResult)
16161616
}
1617+
1618+
quietResult, err := executor.Execute(ctx, contracts.ToolUse{
1619+
ID: "toolu_grep_quiet",
1620+
Name: "Grep",
1621+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"content","-q":"true","--stats":true}`),
1622+
}, nil)
1623+
if err != nil {
1624+
t.Fatal(err)
1625+
}
1626+
if quietResult.Content != "" ||
1627+
quietResult.StructuredContent["quiet"] != true ||
1628+
quietResult.StructuredContent["stats_enabled"] != false ||
1629+
quietResult.StructuredContent["total_matches"] != 1 {
1630+
t.Fatalf("quiet result = %#v", quietResult)
1631+
}
1632+
1633+
noQuietResult, err := executor.Execute(ctx, contracts.ToolUse{
1634+
ID: "toolu_grep_no_quiet_override",
1635+
Name: "Grep",
1636+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"content","quiet":true,"--no-quiet":true,"head_limit":1}`),
1637+
}, nil)
1638+
if err != nil {
1639+
t.Fatal(err)
1640+
}
1641+
if noQuietResult.Content != "a.txt:1:Needle Needle" ||
1642+
noQuietResult.StructuredContent["quiet"] != false ||
1643+
noQuietResult.StructuredContent["no_quiet"] != true {
1644+
t.Fatalf("no-quiet override result = %#v", noQuietResult)
1645+
}
16171646
}
16181647

16191648
func TestGrepToolFilesMode(t *testing.T) {

0 commit comments

Comments
 (0)