Skip to content

Commit d82b27a

Browse files
author
SqlRush
committed
Add Grep passthru output aliases
1 parent e7a46b0 commit d82b27a

4 files changed

Lines changed: 70 additions & 5 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ test/parity/ # golden tests against TS/official behavior
208208
- 本轮补充:Grep 路径过滤现在支持 ripgrep 风格 `--glob`/`-g``--type`/`-t` aliases,执行和 structured content 都统一使用归一化后的 glob/type 过滤值。
209209
- 本轮补充:Grep `glob`/`--glob`/`-g` 路径过滤现在支持 ripgrep 风格 `!pattern` 排除规则,可与正向 glob、逗号/空白多 pattern 和 brace alternation 组合;只有排除规则时默认包含未被排除的路径。
210210
- 本轮补充:Grep 搜索现在支持 ripgrep 风格 `text`/`--text`/`-a`,可显式把二进制扩展名文件按文本读取参与匹配,structured content 会回传 `text` 状态;`-a` 同样兼容 quoted semantic boolean。
211+
- 本轮补充:Grep content 输出现在支持 ripgrep 风格 `passthru`/`passthrough`/`--passthru`/`--passthrough`,可输出被搜索文件的全部行并保留匹配行标记;启用后按官方语义覆盖 context 行数,quoted semantic boolean 同样兼容。
211212
- 本轮补充:Grep 常用布尔参数继续补齐 ripgrep 长参数 aliases,覆盖 `--line-number``--ignore-case``--fixed-strings``--word-regexp``--invert-match``--only-matching`,并兼容 quoted semantic boolean。
212213
- 本轮补充:Grep multiline 搜索现在支持 ripgrep 风格 `-U``--multiline``multiline-dotall``--multiline-dotall` aliases,统一映射到既有跨行 dotall 匹配逻辑并兼容 quoted semantic boolean。
213214
- 本轮补充: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
@@ -135,6 +135,7 @@ Covered behavior:
135135
- `Grep` now accepts ripgrep-style result ordering through `sort`/`--sort` and `sortr`/`--sortr`, covering path, modified-time, and none ordering for files/content/count modes with structured sort metadata.
136136
- `Grep` glob filtering now accepts ripgrep-style negated `!pattern` rules, including combinations with positive patterns, comma/whitespace splitting, and brace-expanded globs.
137137
- `Grep` now accepts `text`/`--text`/`-a` to search binary-extension files as text while preserving the default binary-extension skip behavior.
138+
- `Grep` content output now accepts `passthru`/`passthrough`/`--passthru`/`--passthrough`, printing all searched lines while preserving matched-line markers and overriding context counts.
138139
- `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.
139140
- `Read`/`Edit` now coerce quoted semantic strings for `offset`/`limit` and `replace_all`, including whole-decimal numeric strings such as `"2.0"` for integer fields while keeping fractional values rejected.
140141
- `Bash`/`BashOutput` now coerce quoted semantic strings for `timeout`, `run_in_background`/`runInBackground`, and `tail_lines`/`tailLines`, matching official SDK-style number/boolean inputs without relaxing unknown-field validation.

internal/tools/file/search_tools.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ var allowedGrepInputKeys = map[string]struct{}{
4343
"word_regexp": {}, "wordRegexp": {}, "word-regexp": {}, "--word-regexp": {}, "-w": {},
4444
"invert_match": {}, "invertMatch": {}, "invert-match": {}, "--invert-match": {}, "-v": {},
4545
"only_matching": {}, "onlyMatching": {}, "only-matching": {}, "--only-matching": {}, "-o": {},
46+
"passthru": {}, "passthrough": {}, "--passthru": {}, "--passthrough": {},
4647
"files_with_match": {}, "filesWithMatch": {}, "files-with-match": {}, "--files-with-match": {}, "files_with_matches": {}, "filesWithMatches": {}, "files-with-matches": {}, "--files-with-matches": {}, "-l": {},
4748
"files_without_match": {}, "filesWithoutMatch": {}, "files-without-match": {}, "--files-without-match": {}, "files_without_matches": {}, "filesWithoutMatches": {}, "files-without-matches": {}, "--files-without-matches": {}, "-L": {},
4849
"count": {}, "--count": {}, "-c": {},
@@ -68,6 +69,7 @@ var grepSemanticBooleanKeys = map[string]struct{}{
6869
"word_regexp": {}, "wordRegexp": {}, "word-regexp": {}, "--word-regexp": {}, "-w": {},
6970
"invert_match": {}, "invertMatch": {}, "invert-match": {}, "--invert-match": {}, "-v": {},
7071
"only_matching": {}, "onlyMatching": {}, "only-matching": {}, "--only-matching": {}, "-o": {},
72+
"passthru": {}, "passthrough": {}, "--passthru": {}, "--passthrough": {},
7173
"files_with_match": {}, "filesWithMatch": {}, "files-with-match": {}, "--files-with-match": {}, "files_with_matches": {}, "filesWithMatches": {}, "files-with-matches": {}, "--files-with-matches": {}, "-l": {},
7274
"files_without_match": {}, "filesWithoutMatch": {}, "files-without-match": {}, "--files-without-match": {}, "files_without_matches": {}, "filesWithoutMatches": {}, "files-without-matches": {}, "--files-without-matches": {}, "-L": {},
7375
"count": {}, "--count": {}, "-c": {},
@@ -175,6 +177,10 @@ type grepInput struct {
175177
OnlyMatchingDash bool `json:"only-matching,omitempty"`
176178
LongOnlyMatching bool `json:"--only-matching,omitempty"`
177179
ShortOnlyMatching bool `json:"-o,omitempty"`
180+
Passthru bool `json:"passthru,omitempty"`
181+
Passthrough bool `json:"passthrough,omitempty"`
182+
LongPassthru bool `json:"--passthru,omitempty"`
183+
LongPassthrough bool `json:"--passthrough,omitempty"`
178184
FilesWithMatch bool `json:"files_with_match,omitempty"`
179185
FilesWithMatchAlt bool `json:"filesWithMatch,omitempty"`
180186
FilesWithMatchDash bool `json:"files-with-match,omitempty"`
@@ -239,6 +245,7 @@ type grepOptions struct {
239245
Multiline bool
240246
InvertMatch bool
241247
OnlyMatching bool
248+
Passthru bool
242249
CountMatches bool
243250
ColumnNumbers bool
244251
Text bool
@@ -396,6 +403,10 @@ func NewGrepTool() tool.Tool {
396403
"only-matching": map[string]any{"type": "boolean"},
397404
"--only-matching": map[string]any{"type": "boolean"},
398405
"-o": map[string]any{"type": "boolean"},
406+
"passthru": map[string]any{"type": "boolean"},
407+
"passthrough": map[string]any{"type": "boolean"},
408+
"--passthru": map[string]any{"type": "boolean"},
409+
"--passthrough": map[string]any{"type": "boolean"},
399410
"files_with_match": map[string]any{
400411
"type": "boolean",
401412
},
@@ -462,7 +473,7 @@ func NewGrepTool() tool.Tool {
462473
},
463474
},
464475
PromptFunc: func(tool.PromptContext) (string, error) {
465-
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, 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 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, 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
476+
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, only_matching/-o/--only-matching matched-text output, and passthru/--passthru/--passthrough all-line 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 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, 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
466477
},
467478
NormalizeFunc: normalizeGrepRawInput,
468479
ValidateFunc: validateGrep,
@@ -614,12 +625,18 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
614625
before = 0
615626
after = 0
616627
}
617-
onlyMatching := grepOnlyMatching(input) && mode == "content" && !grepInvertMatch(input)
628+
invertMatch := grepInvertMatch(input)
629+
onlyMatching := grepOnlyMatching(input) && mode == "content" && !invertMatch
618630
if onlyMatching {
619631
before = 0
620632
after = 0
621633
}
622-
countMatches := grepCountMatches(input) && mode == "count" && !grepInvertMatch(input)
634+
passthru := grepPassthru(input) && mode == "content" && !onlyMatching
635+
if passthru {
636+
before = 0
637+
after = 0
638+
}
639+
countMatches := grepCountMatches(input) && mode == "count" && !invertMatch
623640
sortMode, sortReverse, sortExplicit, err := grepSort(input)
624641
if err != nil {
625642
return contracts.ToolResult{}, err
@@ -634,8 +651,9 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
634651
AfterContext: after,
635652
LineNumbers: grepLineNumbers(input, mode),
636653
Multiline: grepMultiline(input),
637-
InvertMatch: grepInvertMatch(input),
654+
InvertMatch: invertMatch,
638655
OnlyMatching: onlyMatching,
656+
Passthru: passthru,
639657
CountMatches: countMatches,
640658
ColumnNumbers: grepColumnNumbers(input),
641659
Text: grepText(input),
@@ -679,8 +697,9 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
679697
"fixed_strings": grepFixedStrings(input),
680698
"text": options.Text,
681699
"word_regexp": grepWordRegexp(input),
682-
"invert_match": grepInvertMatch(input),
700+
"invert_match": invertMatch,
683701
"only_matching": onlyMatching,
702+
"passthru": passthru,
684703
"files_with_matches": mode == "files_with_matches",
685704
"files_without_match": mode == "files_without_matches",
686705
"count": mode == "count",
@@ -970,6 +989,11 @@ func grepFileMatches(path string, content string, expr *regexp.Regexp, options g
970989
} else {
971990
markLineMatches(lines, expr, options.MaxCount, options.BeforeContext, options.AfterContext, options.InvertMatch, matched, included)
972991
}
992+
if options.Passthru {
993+
for i := range lines {
994+
included[i] = true
995+
}
996+
}
973997
matches := make([]grepMatch, 0, len(included))
974998
for i := range lines {
975999
if !included[i] {
@@ -1556,6 +1580,13 @@ func grepOnlyMatching(input grepInput) bool {
15561580
input.ShortOnlyMatching
15571581
}
15581582

1583+
func grepPassthru(input grepInput) bool {
1584+
return input.Passthru ||
1585+
input.Passthrough ||
1586+
input.LongPassthru ||
1587+
input.LongPassthrough
1588+
}
1589+
15591590
func grepMultiline(input grepInput) bool {
15601591
return input.Multiline ||
15611592
input.LongMultiline ||

internal/tools/file/tools_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,6 +1802,38 @@ func TestGrepToolContentContextAndPagination(t *testing.T) {
18021802
t.Fatalf("short context structured content = %#v", shortContextResult.StructuredContent)
18031803
}
18041804

1805+
passthruResult, err := executor.Execute(ctx, contracts.ToolUse{
1806+
ID: "toolu_grep_passthru",
1807+
Name: "Grep",
1808+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"content","--passthru":true,"context":1}`),
1809+
}, nil)
1810+
if err != nil {
1811+
t.Fatal(err)
1812+
}
1813+
if passthruResult.Content != wantContext ||
1814+
passthruResult.StructuredContent["passthru"] != true ||
1815+
passthruResult.StructuredContent["before_context"] != 0 ||
1816+
passthruResult.StructuredContent["after_context"] != 0 {
1817+
t.Fatalf("passthru result = %#v", passthruResult)
1818+
}
1819+
passthruMatches := passthruResult.StructuredContent["matches"].([]map[string]any)
1820+
if len(passthruMatches) != 6 || passthruMatches[0]["matched"] != false || passthruMatches[1]["matched"] != true {
1821+
t.Fatalf("passthru structured matches = %#v", passthruMatches)
1822+
}
1823+
1824+
passthroughResult, err := executor.Execute(ctx, contracts.ToolUse{
1825+
ID: "toolu_grep_passthrough_alias",
1826+
Name: "Grep",
1827+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"content","passthrough":"true","head_limit":1}`),
1828+
}, nil)
1829+
if err != nil {
1830+
t.Fatal(err)
1831+
}
1832+
wantPassthrough := "a.txt-1-one\n\n[Showing results with pagination = limit: 1]"
1833+
if passthroughResult.Content != wantPassthrough || passthroughResult.StructuredContent["passthru"] != true {
1834+
t.Fatalf("passthrough alias result = %#v", passthroughResult)
1835+
}
1836+
18051837
pagedResult, err := executor.Execute(ctx, contracts.ToolUse{
18061838
ID: "toolu_grep_paged",
18071839
Name: "Grep",

0 commit comments

Comments
 (0)