Skip to content

Commit bee46a9

Browse files
author
SqlRush
committed
Support grep long boolean aliases
1 parent f50319e commit bee46a9

4 files changed

Lines changed: 138 additions & 18 deletions

File tree

docs/cc-100-roadmap.md

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

787787
本轮补充:`Grep` 路径过滤现在支持 ripgrep 风格 `--glob`/`-g``--type`/`-t` aliases,执行和 structured content 都统一使用归一化后的 glob/type 过滤值。
788788

789+
本轮补充:`Grep` 常用布尔参数继续补齐 ripgrep 长参数 aliases,覆盖 `--line-number``--ignore-case``--fixed-strings``--word-regexp``--invert-match``--only-matching`,并兼容 quoted semantic boolean。
790+
789791
本轮补充:`Grep` 搜索现在支持 `no_ignore`/`noIgnore`/`no-ignore`/`--no-ignore`,可跳过 `.gitignore`/`.ignore` 规则,同时继续排除 VCS metadata 目录并保留 `Read(...)` deny 额外 ignore 保护;`--no-ignore` 兼容 quoted boolean。
790792

791793
本轮补充:`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
@@ -189,6 +189,7 @@ test/parity/ # golden tests against TS/official behavior
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`
191191
- 本轮补充:Grep 路径过滤现在支持 ripgrep 风格 `--glob`/`-g``--type`/`-t` aliases,执行和 structured content 都统一使用归一化后的 glob/type 过滤值。
192+
- 本轮补充:Grep 常用布尔参数继续补齐 ripgrep 长参数 aliases,覆盖 `--line-number``--ignore-case``--fixed-strings``--word-regexp``--invert-match``--only-matching`,并兼容 quoted semantic boolean。
192193
- 本轮补充:Grep 搜索现在支持 `no_ignore`/`noIgnore`/`no-ignore`/`--no-ignore`,可跳过 `.gitignore`/`.ignore` 规则,同时继续排除 VCS metadata 目录并保留 Read deny 额外 ignore 保护;`--no-ignore` 兼容 quoted boolean。
193194
- 本轮补充:Grep 的 `files_with_matches` 输出现在按官方行为使用文件修改时间倒序排序,mtime 相同再按路径排序;分页和 `head_limit` 会在排序后应用。
194195
- 本轮补充:Glob/Grep 搜索遍历现在会读取 permission context 中的 `Read(...)` deny 规则,并把对应 basename/path/directory pattern 作为额外 ignore rule,避免被禁止读取的文件出现在搜索结果中。

internal/tools/file/search_tools.go

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ var allowedGrepInputKeys = map[string]struct{}{
3030
"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": {},
33-
"context": {}, "-C": {}, "before_context": {}, "beforeContext": {}, "-B": {}, "after_context": {}, "afterContext": {}, "-A": {}, "line_numbers": {}, "lineNumbers": {}, "-n": {},
34-
"ignore_case": {}, "case_insensitive": {}, "caseInsensitive": {}, "-i": {},
35-
"fixed_strings": {}, "fixedStrings": {}, "-F": {}, "multiline": {},
36-
"word_regexp": {}, "wordRegexp": {}, "word-regexp": {}, "-w": {},
37-
"invert_match": {}, "invertMatch": {}, "invert-match": {}, "-v": {},
38-
"only_matching": {}, "onlyMatching": {}, "only-matching": {}, "-o": {},
33+
"context": {}, "-C": {}, "before_context": {}, "beforeContext": {}, "-B": {}, "after_context": {}, "afterContext": {}, "-A": {}, "line_numbers": {}, "lineNumbers": {}, "line-number": {}, "--line-number": {}, "-n": {},
34+
"ignore_case": {}, "case_insensitive": {}, "caseInsensitive": {}, "ignore-case": {}, "--ignore-case": {}, "-i": {},
35+
"fixed_strings": {}, "fixedStrings": {}, "fixed-strings": {}, "--fixed-strings": {}, "-F": {}, "multiline": {},
36+
"word_regexp": {}, "wordRegexp": {}, "word-regexp": {}, "--word-regexp": {}, "-w": {},
37+
"invert_match": {}, "invertMatch": {}, "invert-match": {}, "--invert-match": {}, "-v": {},
38+
"only_matching": {}, "onlyMatching": {}, "only-matching": {}, "--only-matching": {}, "-o": {},
3939
"files_with_match": {}, "filesWithMatch": {}, "files-with-match": {}, "--files-with-match": {}, "files_with_matches": {}, "filesWithMatches": {}, "files-with-matches": {}, "--files-with-matches": {}, "-l": {},
4040
"files_without_match": {}, "filesWithoutMatch": {}, "files-without-match": {}, "--files-without-match": {}, "files_without_matches": {}, "filesWithoutMatches": {}, "files-without-matches": {}, "--files-without-matches": {}, "-L": {},
4141
"count_matches": {}, "countMatches": {}, "count-matches": {}, "--count-matches": {},
@@ -49,12 +49,12 @@ var grepSemanticNumberKeys = map[string]struct{}{
4949
}
5050

5151
var grepSemanticBooleanKeys = map[string]struct{}{
52-
"line_numbers": {}, "lineNumbers": {}, "-n": {},
53-
"ignore_case": {}, "case_insensitive": {}, "caseInsensitive": {}, "-i": {},
54-
"fixed_strings": {}, "fixedStrings": {}, "-F": {}, "multiline": {},
55-
"word_regexp": {}, "wordRegexp": {}, "word-regexp": {}, "-w": {},
56-
"invert_match": {}, "invertMatch": {}, "invert-match": {}, "-v": {},
57-
"only_matching": {}, "onlyMatching": {}, "only-matching": {}, "-o": {},
52+
"line_numbers": {}, "lineNumbers": {}, "line-number": {}, "--line-number": {}, "-n": {},
53+
"ignore_case": {}, "case_insensitive": {}, "caseInsensitive": {}, "ignore-case": {}, "--ignore-case": {}, "-i": {},
54+
"fixed_strings": {}, "fixedStrings": {}, "fixed-strings": {}, "--fixed-strings": {}, "-F": {}, "multiline": {},
55+
"word_regexp": {}, "wordRegexp": {}, "word-regexp": {}, "--word-regexp": {}, "-w": {},
56+
"invert_match": {}, "invertMatch": {}, "invert-match": {}, "--invert-match": {}, "-v": {},
57+
"only_matching": {}, "onlyMatching": {}, "only-matching": {}, "--only-matching": {}, "-o": {},
5858
"files_with_match": {}, "filesWithMatch": {}, "files-with-match": {}, "--files-with-match": {}, "files_with_matches": {}, "filesWithMatches": {}, "files-with-matches": {}, "--files-with-matches": {}, "-l": {},
5959
"files_without_match": {}, "filesWithoutMatch": {}, "files-without-match": {}, "--files-without-match": {}, "files_without_matches": {}, "filesWithoutMatches": {}, "files-without-matches": {}, "--files-without-matches": {}, "-L": {},
6060
"count_matches": {}, "countMatches": {}, "count-matches": {}, "--count-matches": {},
@@ -98,25 +98,34 @@ type grepInput struct {
9898
ShortAfterContext *int `json:"-A,omitempty"`
9999
LineNumbers *bool `json:"line_numbers,omitempty"`
100100
LineNumbersAlt *bool `json:"lineNumbers,omitempty"`
101+
LineNumbersDash *bool `json:"line-number,omitempty"`
102+
LongLineNumbers *bool `json:"--line-number,omitempty"`
101103
ShortLineNumbers *bool `json:"-n,omitempty"`
102104
IgnoreCase bool `json:"ignore_case,omitempty"`
103105
CaseInsensitive bool `json:"case_insensitive,omitempty"`
104106
CaseInsensitiveAlt bool `json:"caseInsensitive,omitempty"`
107+
IgnoreCaseDash bool `json:"ignore-case,omitempty"`
108+
LongIgnoreCase bool `json:"--ignore-case,omitempty"`
105109
ShortIgnoreCase bool `json:"-i,omitempty"`
106110
FixedStrings bool `json:"fixed_strings,omitempty"`
107111
FixedStringsAlt bool `json:"fixedStrings,omitempty"`
112+
FixedStringsDash bool `json:"fixed-strings,omitempty"`
113+
LongFixedStrings bool `json:"--fixed-strings,omitempty"`
108114
ShortFixedStrings bool `json:"-F,omitempty"`
109115
WordRegexp bool `json:"word_regexp,omitempty"`
110116
WordRegexpAlt bool `json:"wordRegexp,omitempty"`
111117
WordRegexpDash bool `json:"word-regexp,omitempty"`
118+
LongWordRegexp bool `json:"--word-regexp,omitempty"`
112119
ShortWordRegexp bool `json:"-w,omitempty"`
113120
InvertMatch bool `json:"invert_match,omitempty"`
114121
InvertMatchAlt bool `json:"invertMatch,omitempty"`
115122
InvertMatchDash bool `json:"invert-match,omitempty"`
123+
LongInvertMatch bool `json:"--invert-match,omitempty"`
116124
ShortInvertMatch bool `json:"-v,omitempty"`
117125
OnlyMatching bool `json:"only_matching,omitempty"`
118126
OnlyMatchingAlt bool `json:"onlyMatching,omitempty"`
119127
OnlyMatchingDash bool `json:"only-matching,omitempty"`
128+
LongOnlyMatching bool `json:"--only-matching,omitempty"`
120129
ShortOnlyMatching bool `json:"-o,omitempty"`
121130
FilesWithMatch bool `json:"files_with_match,omitempty"`
122131
FilesWithMatchAlt bool `json:"filesWithMatch,omitempty"`
@@ -263,25 +272,34 @@ func NewGrepTool() tool.Tool {
263272
"-A": map[string]any{"type": "integer"},
264273
"line_numbers": map[string]any{"type": "boolean"},
265274
"lineNumbers": map[string]any{"type": "boolean"},
275+
"line-number": map[string]any{"type": "boolean"},
276+
"--line-number": map[string]any{"type": "boolean"},
266277
"-n": map[string]any{"type": "boolean"},
267278
"ignore_case": map[string]any{"type": "boolean"},
268279
"case_insensitive": map[string]any{"type": "boolean"},
269280
"caseInsensitive": map[string]any{"type": "boolean"},
281+
"ignore-case": map[string]any{"type": "boolean"},
282+
"--ignore-case": map[string]any{"type": "boolean"},
270283
"-i": map[string]any{"type": "boolean"},
271284
"fixed_strings": map[string]any{"type": "boolean"},
272285
"fixedStrings": map[string]any{"type": "boolean"},
286+
"fixed-strings": map[string]any{"type": "boolean"},
287+
"--fixed-strings": map[string]any{"type": "boolean"},
273288
"-F": map[string]any{"type": "boolean"},
274289
"word_regexp": map[string]any{"type": "boolean"},
275290
"wordRegexp": map[string]any{"type": "boolean"},
276291
"word-regexp": map[string]any{"type": "boolean"},
292+
"--word-regexp": map[string]any{"type": "boolean"},
277293
"-w": map[string]any{"type": "boolean"},
278294
"invert_match": map[string]any{"type": "boolean"},
279295
"invertMatch": map[string]any{"type": "boolean"},
280296
"invert-match": map[string]any{"type": "boolean"},
297+
"--invert-match": map[string]any{"type": "boolean"},
281298
"-v": map[string]any{"type": "boolean"},
282299
"only_matching": map[string]any{"type": "boolean"},
283300
"onlyMatching": map[string]any{"type": "boolean"},
284301
"only-matching": map[string]any{"type": "boolean"},
302+
"--only-matching": map[string]any{"type": "boolean"},
285303
"-o": map[string]any{"type": "boolean"},
286304
"files_with_match": map[string]any{
287305
"type": "boolean",
@@ -338,7 +356,7 @@ func NewGrepTool() tool.Tool {
338356
},
339357
},
340358
PromptFunc: func(tool.PromptContext) (string, error) {
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
359+
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, and files_without_match or -L to list files without matches. 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
342360
},
343361
NormalizeFunc: normalizeGrepRawInput,
344362
ValidateFunc: validateGrep,
@@ -1217,23 +1235,44 @@ func grepTypeFilter(input grepInput) string {
12171235
}
12181236

12191237
func grepCaseInsensitive(input grepInput) bool {
1220-
return input.IgnoreCase || input.CaseInsensitive || input.CaseInsensitiveAlt || input.ShortIgnoreCase
1238+
return input.IgnoreCase ||
1239+
input.CaseInsensitive ||
1240+
input.CaseInsensitiveAlt ||
1241+
input.IgnoreCaseDash ||
1242+
input.LongIgnoreCase ||
1243+
input.ShortIgnoreCase
12211244
}
12221245

12231246
func grepFixedStrings(input grepInput) bool {
1224-
return input.FixedStrings || input.FixedStringsAlt || input.ShortFixedStrings
1247+
return input.FixedStrings ||
1248+
input.FixedStringsAlt ||
1249+
input.FixedStringsDash ||
1250+
input.LongFixedStrings ||
1251+
input.ShortFixedStrings
12251252
}
12261253

12271254
func grepWordRegexp(input grepInput) bool {
1228-
return input.WordRegexp || input.WordRegexpAlt || input.WordRegexpDash || input.ShortWordRegexp
1255+
return input.WordRegexp ||
1256+
input.WordRegexpAlt ||
1257+
input.WordRegexpDash ||
1258+
input.LongWordRegexp ||
1259+
input.ShortWordRegexp
12291260
}
12301261

12311262
func grepInvertMatch(input grepInput) bool {
1232-
return input.InvertMatch || input.InvertMatchAlt || input.InvertMatchDash || input.ShortInvertMatch
1263+
return input.InvertMatch ||
1264+
input.InvertMatchAlt ||
1265+
input.InvertMatchDash ||
1266+
input.LongInvertMatch ||
1267+
input.ShortInvertMatch
12331268
}
12341269

12351270
func grepOnlyMatching(input grepInput) bool {
1236-
return input.OnlyMatching || input.OnlyMatchingAlt || input.OnlyMatchingDash || input.ShortOnlyMatching
1271+
return input.OnlyMatching ||
1272+
input.OnlyMatchingAlt ||
1273+
input.OnlyMatchingDash ||
1274+
input.LongOnlyMatching ||
1275+
input.ShortOnlyMatching
12371276
}
12381277

12391278
func grepFilesWithMatches(input grepInput) bool {
@@ -1278,6 +1317,12 @@ func grepLineNumbers(input grepInput, mode string) bool {
12781317
if input.LineNumbersAlt != nil {
12791318
return *input.LineNumbersAlt
12801319
}
1320+
if input.LineNumbersDash != nil {
1321+
return *input.LineNumbersDash
1322+
}
1323+
if input.LongLineNumbers != nil {
1324+
return *input.LongLineNumbers
1325+
}
12811326
if input.ShortLineNumbers == nil {
12821327
return true
12831328
}

internal/tools/file/tools_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,18 @@ func TestGrepToolContentContextAndPagination(t *testing.T) {
15801580
t.Fatalf("lineNumbers alias result = %#v", camelLineNumberResult)
15811581
}
15821582

1583+
longLineNumberResult, err := executor.Execute(ctx, contracts.ToolUse{
1584+
ID: "toolu_grep_line_number_long_alias",
1585+
Name: "Grep",
1586+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"content","--line-number":"false","head_limit":1}`),
1587+
}, nil)
1588+
if err != nil {
1589+
t.Fatal(err)
1590+
}
1591+
if longLineNumberResult.Content != wantNoLineNumber || longLineNumberResult.StructuredContent["line_numbers"] != false {
1592+
t.Fatalf("long line-number alias result = %#v", longLineNumberResult)
1593+
}
1594+
15831595
shortContextResult, err := executor.Execute(ctx, contracts.ToolUse{
15841596
ID: "toolu_grep_short_context",
15851597
Name: "Grep",
@@ -1797,6 +1809,18 @@ func TestGrepToolCaseInsensitiveAndValidation(t *testing.T) {
17971809
t.Fatalf("semantic bool case-insensitive result = %#v", semanticBoolResult)
17981810
}
17991811

1812+
longIgnoreCaseResult, err := executor.Execute(ctx, contracts.ToolUse{
1813+
ID: "toolu_grep_long_ignore_case",
1814+
Name: "Grep",
1815+
Input: json.RawMessage(`{"pattern":"alpha","--ignore-case":"true"}`),
1816+
}, nil)
1817+
if err != nil {
1818+
t.Fatal(err)
1819+
}
1820+
if longIgnoreCaseResult.Content != "Found 1 file\nmixed.txt" || longIgnoreCaseResult.StructuredContent["case_insensitive"] != true {
1821+
t.Fatalf("long ignore-case result = %#v", longIgnoreCaseResult)
1822+
}
1823+
18001824
ignoredContextResult, err := executor.Execute(ctx, contracts.ToolUse{
18011825
ID: "toolu_grep_ignored_context",
18021826
Name: "Grep",
@@ -1862,6 +1886,18 @@ func TestGrepToolFixedStrings(t *testing.T) {
18621886
if fixedResult.Content != "literal.txt:1:a+b" || fixedResult.StructuredContent["fixed_strings"] != true {
18631887
t.Fatalf("fixed result = %#v", fixedResult)
18641888
}
1889+
1890+
longFixedResult, err := executor.Execute(ctx, contracts.ToolUse{
1891+
ID: "toolu_grep_long_fixed",
1892+
Name: "Grep",
1893+
Input: json.RawMessage(`{"pattern":"a+b","output_mode":"content","--fixed-strings":"true"}`),
1894+
}, nil)
1895+
if err != nil {
1896+
t.Fatal(err)
1897+
}
1898+
if longFixedResult.Content != "literal.txt:1:a+b" || longFixedResult.StructuredContent["fixed_strings"] != true {
1899+
t.Fatalf("long fixed result = %#v", longFixedResult)
1900+
}
18651901
}
18661902

18671903
func TestGrepToolOnlyMatching(t *testing.T) {
@@ -1906,6 +1942,18 @@ func TestGrepToolOnlyMatching(t *testing.T) {
19061942
if shortResult.Content != wantShort || shortResult.StructuredContent["only_matching"] != true || shortResult.StructuredContent["line_numbers"] != false {
19071943
t.Fatalf("short only-matching result = %#v", shortResult)
19081944
}
1945+
1946+
longResult, err := executor.Execute(ctx, contracts.ToolUse{
1947+
ID: "toolu_grep_only_matching_long",
1948+
Name: "Grep",
1949+
Input: json.RawMessage(`{"pattern":"ID-[0-9]+","outputMode":"content","--only-matching":"true","--line-number":false}`),
1950+
}, nil)
1951+
if err != nil {
1952+
t.Fatal(err)
1953+
}
1954+
if longResult.Content != wantShort || longResult.StructuredContent["only_matching"] != true || longResult.StructuredContent["line_numbers"] != false {
1955+
t.Fatalf("long only-matching result = %#v", longResult)
1956+
}
19091957
}
19101958

19111959
func TestGrepToolWordRegexp(t *testing.T) {
@@ -1973,6 +2021,18 @@ func TestGrepToolWordRegexp(t *testing.T) {
19732021
if shortSemanticResult.Content != wantCount || shortSemanticResult.StructuredContent["word_regexp"] != true {
19742022
t.Fatalf("short semantic word regexp result = %#v", shortSemanticResult)
19752023
}
2024+
2025+
longResult, err := executor.Execute(ctx, contracts.ToolUse{
2026+
ID: "toolu_grep_word_regexp_long",
2027+
Name: "Grep",
2028+
Input: json.RawMessage(`{"pattern":"cat","output_mode":"count","--word-regexp":"true"}`),
2029+
}, nil)
2030+
if err != nil {
2031+
t.Fatal(err)
2032+
}
2033+
if longResult.Content != wantCount || longResult.StructuredContent["word_regexp"] != true {
2034+
t.Fatalf("long word-regexp result = %#v", longResult)
2035+
}
19762036
}
19772037

19782038
func TestGrepToolInvertMatch(t *testing.T) {
@@ -2043,6 +2103,18 @@ func TestGrepToolInvertMatch(t *testing.T) {
20432103
if shortResult.Content != "Found 1 file\none.txt" || shortResult.StructuredContent["invert_match"] != true {
20442104
t.Fatalf("short invert-match result = %#v", shortResult)
20452105
}
2106+
2107+
longResult, err := executor.Execute(ctx, contracts.ToolUse{
2108+
ID: "toolu_grep_invert_match_long_files",
2109+
Name: "Grep",
2110+
Input: json.RawMessage(`{"pattern":"Needle","--invert-match":"true"}`),
2111+
}, nil)
2112+
if err != nil {
2113+
t.Fatal(err)
2114+
}
2115+
if longResult.Content != "Found 1 file\none.txt" || longResult.StructuredContent["invert_match"] != true {
2116+
t.Fatalf("long invert-match result = %#v", longResult)
2117+
}
20462118
}
20472119

20482120
func TestGrepToolMultiline(t *testing.T) {

0 commit comments

Comments
 (0)