Skip to content

Commit 7321bd9

Browse files
author
SqlRush
committed
Support Grep line number control
1 parent 465e594 commit 7321bd9

5 files changed

Lines changed: 40 additions & 9 deletions

File tree

‎docs/cc-100-roadmap.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
512512
- 文本版 `Read`、PDF text/page-selection 初版(含常见 Page/Contents 间接对象、Pages/Kids 页序、FlateDecode 文本流和 UTF-16 BOM 字符串)、PNG/JPEG/GIF/WebP image Read、Jupyter notebook cell 渲染初版、Read 大文本 tool-result budget 截断/落盘、`Write`、`Edit` 初版已完成。
513513
- 已覆盖读前写、mtime stale guard、唯一匹配、`replace_all`、Write/Edit structured diff hunks、`.claude/settings.json`/`settings.local.json` 写前 JSON/语义校验、team-memory secret guard、Read 去重、跨 tool round read-state。
514514
- `Bash` 初版已完成,支持 command/timeout/description 输入校验、`/bin/sh -c` 执行、stdout/stderr/exit code/timeout 结构化结果、动态 read-only/concurrency-safe/destructive 分类、Git branch/tag/remote/push 参数级安全分类、`find -delete/-exec rm` 与 `xargs rm` 破坏性分类、权限规则接入、后台启动、同会话 `BashOutput` 输出读取和 `KillBash` 取消。
515-
- `Glob`/`Grep` 纯 Go 初版已完成,支持 `**` 递归 glob、基础 ignored dirs、层级 `.gitignore`/`.ignore`、mtime/path 排序、Grep regex/fixed string (`fixed_strings`/`-F`)、glob/type 过滤、`output_mode`/`outputMode` 的 `files_with_matches`/`content`/`count` 输出模式、`context`/`before_context`/`after_context` 和 `-C`/`-B`/`-A` 上下文行、`offset`/`head_limit` 分页和大小写不敏感搜索。
515+
- `Glob`/`Grep` 纯 Go 初版已完成,支持 `**` 递归 glob、基础 ignored dirs、层级 `.gitignore`/`.ignore`、mtime/path 排序、Grep regex/fixed string (`fixed_strings`/`-F`)、glob/type 过滤、`output_mode`/`outputMode` 的 `files_with_matches`/`content`/`count` 输出模式、`context`/`before_context`/`after_context` 和 `-C`/`-B`/`-A` 上下文行、`-n` line-number 控制、`offset`/`head_limit` 分页和大小写不敏感搜索。
516516
- `TodoWrite` 初版已完成,支持完整 todo list 写入、状态/优先级校验、重复 id 拒绝、单个 `in_progress` 约束、结构化结果、tool metadata 状态保存和 session-scoped 本地持久化/恢复。
517517
- `WebFetch` 初版已完成,支持 URL/timeout/max_bytes 输入校验、HTTP GET、HEAD preflight、metadata/raw `skipWebFetchPreflight` skip-preflight、二进制 preflight 跳过 GET、文本/二进制判定、截断、非 2xx error 标记、结构化结果、HTML-to-text rendering、prompt-focused excerpt 和 `WebFetch(domain:...)` 权限规则适配。
518518
- `WebSearch` HTML 搜索适配初版已完成,支持 query/max_results/timeout/domain filters 输入校验、可注入搜索 endpoint、DuckDuckGo HTML 链接解析、DuckDuckGo result snippet 抽取、domain allow/block 过滤、结构化结果和 query 权限规则匹配。

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ test/parity/ # golden tests against TS/official behavior
168168

169169
- Read/Edit/Write 初版已落地,覆盖文本 Read、PDF text/page-selection 初版(含常见 Page/Contents 间接对象、Pages/Kids 页序、FlateDecode 文本流和 UTF-16 BOM 字符串)、PNG/JPEG/GIF/WebP image Read、Jupyter notebook cell 渲染初版、Read 大文本 tool-result budget 截断/落盘、read-before-write、mtime stale guard、唯一匹配、`replace_all`、Write/Edit structured diff hunks、`.claude/settings.json`/`settings.local.json` 写前 JSON/语义校验、team-memory secret guard、Read 去重和跨 tool round read-state。
170170
- Bash 初版已落地,覆盖 command/timeout/description 输入校验、`/bin/sh -c` 执行、stdout/stderr/exit code/timeout 结构化结果、动态 read-only/concurrency-safe/destructive 分类、Git branch/tag/remote/push 参数级安全分类、`find -delete/-exec rm` 与 `xargs rm` 破坏性分类、权限规则接入、后台启动、同会话 `BashOutput` 输出读取和 `KillBash` 取消;完整 shell parser、真实 sandbox、interrupt、后台任务完整生命周期和官方 golden 仍需继续补。
171-
- Glob/Grep 纯 Go 初版已落地,覆盖 `**` 递归 glob、基础 ignored dirs、层级 `.gitignore`/`.ignore`、mtime/path 排序、Grep regex/fixed string (`fixed_strings`/`-F`)、glob/type 过滤、`output_mode`/`outputMode` 的 `files_with_matches`/`content`/`count` 输出模式、`context`/`before_context`/`after_context` 及 `-C`/`-B`/`-A` 上下文行、`offset`/`head_limit` 分页和大小写不敏感搜索;完整 ripgrep parity 和剩余输出参数仍需继续补。
171+
- Glob/Grep 纯 Go 初版已落地,覆盖 `**` 递归 glob、基础 ignored dirs、层级 `.gitignore`/`.ignore`、mtime/path 排序、Grep regex/fixed string (`fixed_strings`/`-F`)、glob/type 过滤、`output_mode`/`outputMode` 的 `files_with_matches`/`content`/`count` 输出模式、`context`/`before_context`/`after_context` 及 `-C`/`-B`/`-A` 上下文行、`-n` line-number 控制、`offset`/`head_limit` 分页和大小写不敏感搜索;完整 ripgrep parity 和剩余输出参数仍需继续补。
172172
- TodoWrite 初版已落地,覆盖完整 todo list 写入、状态/优先级校验、重复 id 拒绝、单个 `in_progress` 约束、结构化结果、tool metadata 状态保存和 session-scoped 本地持久化/恢复;TUI 同步和官方 golden 仍需继续补。
173173
- WebFetch 初版已落地,覆盖 URL/timeout/max_bytes 输入校验、HTTP GET、HEAD preflight、metadata/raw `skipWebFetchPreflight` skip-preflight、二进制 preflight 跳过 GET、文本/二进制判定、截断、非 2xx error 标记、结构化结果、HTML-to-text rendering、prompt-focused excerpt 和 `WebFetch(domain:...)` 权限规则适配;browser 渲染、完整 prompt-aware summarization 和官方 golden 仍需继续补。
174174
- WebSearch HTML 搜索适配初版已落地,覆盖 query/max_results/timeout/domain filters 输入校验、可注入搜索 endpoint、DuckDuckGo HTML 链接解析、DuckDuckGo result snippet 抽取、domain allow/block 过滤、结构化结果和 query 权限规则匹配;官方搜索后端、ranking parity 和 golden 仍需继续补。

‎docs/first-second-parity-audit.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ After the first/second batch hardening, the Go rewrite now includes initial `int
105105

106106
Covered behavior:
107107

108-
- `Glob`/`Grep` initial pure-Go file search tools, including recursive `**` glob matching, basic ignored directories, hierarchical `.gitignore`/`.ignore`, modified-time/path sorting, regex and fixed-string grep (`fixed_strings`/`-F`), glob/type filtering, `output_mode`/`outputMode` `files_with_matches`/`content`/`count` output modes, `context`/`before_context`/`after_context` and `-C`/`-B`/`-A` content context lines, `offset`/`head_limit` pagination, and case-insensitive search aliases.
108+
- `Glob`/`Grep` initial pure-Go file search tools, including recursive `**` glob matching, basic ignored directories, hierarchical `.gitignore`/`.ignore`, modified-time/path sorting, regex and fixed-string grep (`fixed_strings`/`-F`), glob/type filtering, `output_mode`/`outputMode` `files_with_matches`/`content`/`count` output modes, `context`/`before_context`/`after_context` and `-C`/`-B`/`-A` content context lines, `-n` line-number control, `offset`/`head_limit` pagination, and case-insensitive search aliases.
109109
- `Bash` initial shell execution, including command/timeout/description validation, `/bin/sh -c` execution, stdout/stderr/exit-code/timeout structured results, dynamic read-only/concurrency-safe/destructive classification, Git branch/tag/remote/push argument-aware safety classification, destructive `find -delete/-exec rm` and `xargs rm` classification, permission-rule matching through the existing tool permission adapter, background command start, same-session `BashOutput` reads, and `KillBash` cancellation.
110110
- `TodoWrite` initial todo state, including full-list writes, status/priority validation, duplicate-id rejection, one `in_progress` guard, structured result payloads, tool metadata state storage, and session-scoped local persistence/restore.
111111
- `WebFetch` initial URL fetch behavior, including URL/timeout/max-byte validation, HTTP GET, HEAD preflight, metadata/raw `skipWebFetchPreflight` skip-preflight, binary preflight GET skipping, text/binary detection, truncation, non-2xx error marking, structured result payloads, HTML-to-text rendering, prompt-focused excerpts, and `WebFetch(domain:...)` permission-rule adaptation.

‎internal/tools/file/search_tools.go‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type grepInput struct {
4040
AfterContext *int `json:"after_context,omitempty"`
4141
AfterContextAlt *int `json:"afterContext,omitempty"`
4242
ShortAfterContext *int `json:"-A,omitempty"`
43+
ShortLineNumbers *bool `json:"-n,omitempty"`
4344
CaseInsensitive bool `json:"case_insensitive,omitempty"`
4445
CaseInsensitiveAlt bool `json:"caseInsensitive,omitempty"`
4546
ShortIgnoreCase bool `json:"-i,omitempty"`
@@ -68,6 +69,7 @@ type grepOptions struct {
6869
Offset int
6970
BeforeContext int
7071
AfterContext int
72+
LineNumbers bool
7173
}
7274

7375
func NewGlobTool() tool.Tool {
@@ -134,6 +136,7 @@ func NewGrepTool() tool.Tool {
134136
},
135137
"afterContext": map[string]any{"type": "integer"},
136138
"-A": map[string]any{"type": "integer"},
139+
"-n": map[string]any{"type": "boolean"},
137140
"case_insensitive": map[string]any{"type": "boolean"},
138141
"caseInsensitive": map[string]any{"type": "boolean"},
139142
"-i": map[string]any{"type": "boolean"},
@@ -144,7 +147,7 @@ func NewGrepTool() tool.Tool {
144147
},
145148
},
146149
PromptFunc: func(tool.PromptContext) (string, error) {
147-
return "Searches text files under path using a regular expression or fixed string. output_mode may be files_with_matches, content, or count; glob and type optionally filter file paths. content mode supports context, before_context, after_context, -C, -B, -A, offset, and head_limit pagination. Use fixed_strings or -F for literal matching.", nil
150+
return "Searches text files under path using a regular expression or fixed string. output_mode may be files_with_matches, content, or count; glob and type optionally filter file paths. content mode supports context, before_context, after_context, -C, -B, -A, -n line-number control, offset, and head_limit pagination. Use fixed_strings or -F for literal matching.", nil
148151
},
149152
ValidateFunc: validateGrep,
150153
CallFunc: callGrep,
@@ -266,12 +269,13 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
266269
Offset: grepOffset(input),
267270
BeforeContext: before,
268271
AfterContext: after,
272+
LineNumbers: grepLineNumbers(input, mode),
269273
}
270274
matches, totalMatches, truncated, err := collectGrepMatches(root, input.Glob, input.Type, expr, options)
271275
if err != nil {
272276
return contracts.ToolResult{}, err
273277
}
274-
content := formatGrepMatches(matches, mode)
278+
content := formatGrepMatches(matches, options)
275279
if content == "" {
276280
content = "No matches found"
277281
}
@@ -290,6 +294,7 @@ func callGrep(ctx tool.Context, raw json.RawMessage, _ tool.ProgressSink) (contr
290294
"limit": options.Limit,
291295
"before_context": options.BeforeContext,
292296
"after_context": options.AfterContext,
297+
"line_numbers": options.LineNumbers,
293298
"case_insensitive": grepCaseInsensitive(input),
294299
"fixed_strings": grepFixedStrings(input),
295300
"truncated": truncated,
@@ -310,7 +315,7 @@ func decodeGrep(raw json.RawMessage) (grepInput, error) {
310315
if err := decodeStrict(raw, map[string]struct{}{
311316
"pattern": {}, "path": {}, "glob": {}, "type": {}, "output_mode": {}, "outputMode": {}, "limit": {},
312317
"head_limit": {}, "headLimit": {}, "offset": {},
313-
"context": {}, "-C": {}, "before_context": {}, "beforeContext": {}, "-B": {}, "after_context": {}, "afterContext": {}, "-A": {},
318+
"context": {}, "-C": {}, "before_context": {}, "beforeContext": {}, "-B": {}, "after_context": {}, "afterContext": {}, "-A": {}, "-n": {},
314319
"case_insensitive": {}, "caseInsensitive": {}, "-i": {},
315320
"fixed_strings": {}, "fixedStrings": {}, "-F": {},
316321
}, &input); err != nil {
@@ -462,10 +467,10 @@ func countMatchedLines(matches []grepMatch) int {
462467
return count
463468
}
464469

465-
func formatGrepMatches(matches []grepMatch, mode string) string {
470+
func formatGrepMatches(matches []grepMatch, options grepOptions) string {
466471
lines := make([]string, 0, len(matches))
467472
for _, match := range matches {
468-
switch mode {
473+
switch options.Mode {
469474
case "files_with_matches":
470475
lines = append(lines, match.Path)
471476
case "count":
@@ -475,7 +480,11 @@ func formatGrepMatches(matches []grepMatch, mode string) string {
475480
if !match.Matched {
476481
separator = "-"
477482
}
478-
lines = append(lines, fmt.Sprintf("%s%s%d%s%s", match.Path, separator, match.Line, separator, match.Text))
483+
if options.LineNumbers {
484+
lines = append(lines, fmt.Sprintf("%s%s%d%s%s", match.Path, separator, match.Line, separator, match.Text))
485+
} else {
486+
lines = append(lines, fmt.Sprintf("%s%s%s", match.Path, separator, match.Text))
487+
}
479488
}
480489
}
481490
return strings.Join(lines, "\n")
@@ -528,6 +537,16 @@ func grepFixedStrings(input grepInput) bool {
528537
return input.FixedStrings || input.FixedStringsAlt || input.ShortFixedStrings
529538
}
530539

540+
func grepLineNumbers(input grepInput, mode string) bool {
541+
if mode != "content" {
542+
return false
543+
}
544+
if input.ShortLineNumbers == nil {
545+
return true
546+
}
547+
return *input.ShortLineNumbers
548+
}
549+
531550
func grepTypeExtensions(typeFilter string) ([]string, error) {
532551
typeFilter = strings.TrimSpace(strings.TrimPrefix(typeFilter, "."))
533552
if typeFilter == "" {

‎internal/tools/file/tools_test.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,18 @@ func TestGrepToolContentContextAndPagination(t *testing.T) {
880880
t.Fatalf("structured context matches = %#v", matches)
881881
}
882882

883+
noLineNumberResult, err := executor.Execute(ctx, contracts.ToolUse{
884+
ID: "toolu_grep_no_line_numbers",
885+
Name: "Grep",
886+
Input: json.RawMessage(`{"pattern":"Needle","output_mode":"content","-n":false,"head_limit":1}`),
887+
}, nil)
888+
if err != nil {
889+
t.Fatal(err)
890+
}
891+
if noLineNumberResult.Content != "a.txt:Needle first" || noLineNumberResult.StructuredContent["line_numbers"] != false {
892+
t.Fatalf("no-line-number content = %#v", noLineNumberResult)
893+
}
894+
883895
shortContextResult, err := executor.Execute(ctx, contracts.ToolUse{
884896
ID: "toolu_grep_short_context",
885897
Name: "Grep",

0 commit comments

Comments
 (0)