Skip to content

Commit be71de5

Browse files
committed
refactor(render): 将输出渲染逻辑重构为统一的内容写入接口
- 移除 strings 包依赖,统一使用 render.Content 进行文本输出 - 创建 internal/render 包封装格式化写入功能 - 将 writeAnalyzeText 和 writeRestoreText 函数重写为返回错误 - 更新 README 中的构建说明使用新脚本 - 添加对 report.WriteText 的错误处理测试 - 重构 Text 函数使用新的 WriteText 接口实现
1 parent 099ed04 commit be71de5

9 files changed

Lines changed: 161 additions & 38 deletions

File tree

‎README.en.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ go install github.com/walker1211/histprune/cmd/histprune@latest
3030
histprune version
3131
```
3232

33-
For local development, you can also build a binary:
33+
For local development, you can build a binary with the helper script:
3434

3535
```bash
36-
go build -o histprune ./cmd/histprune
36+
./build.sh
3737
./histprune version
3838
```
3939

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Safe, explainable shell history cleanup in Go.
44

5-
[中文文档](./README.zh-CN.md) | [English Documentation](./README.en.md)
5+
[中文](./README.zh-CN.md) | [English](./README.en.md)
66

77
## Features
88

‎README.zh-CN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ go install github.com/walker1211/histprune/cmd/histprune@latest
3030
histprune version
3131
```
3232

33-
仓库本地开发时也可以构建二进制:
33+
仓库本地开发时可以用脚本构建二进制:
3434

3535
```bash
36-
go build -o histprune ./cmd/histprune
36+
./build.sh
3737
./histprune version
3838
```
3939

‎build.sh‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
cd "$(dirname "$0")" || exit
4+
5+
echo "Building..."
6+
go build -o histprune ./cmd/histprune
7+
echo "Done. Binary: ./histprune"

‎internal/app/app.go‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import (
66
"fmt"
77
"io"
88
"sort"
9-
"strings"
109

1110
"github.com/walker1211/histprune/internal/cli"
1211
"github.com/walker1211/histprune/internal/history"
1312
"github.com/walker1211/histprune/internal/prune"
13+
"github.com/walker1211/histprune/internal/render"
1414
"github.com/walker1211/histprune/internal/report"
1515
"github.com/walker1211/histprune/internal/storage"
1616
)
@@ -61,8 +61,7 @@ func (a App) analyze(ctx context.Context, opts cli.AnalyzeOptions) error {
6161
if opts.JSON {
6262
return writeJSON(a.stdout, summary)
6363
}
64-
_, err = io.WriteString(a.stdout, renderAnalyzeText(summary))
65-
return err
64+
return writeAnalyzeText(a.stdout, summary)
6665
})
6766
}
6867

@@ -107,8 +106,7 @@ func (a App) prune(ctx context.Context, opts cli.PruneOptions) error {
107106
_, err = fmt.Fprintln(a.stdout, text)
108107
return err
109108
}
110-
_, err = io.WriteString(a.stdout, report.Text(summary))
111-
return err
109+
return report.WriteText(a.stdout, summary)
112110
})
113111
}
114112

@@ -168,8 +166,7 @@ func (a App) restore(ctx context.Context, opts cli.RestoreOptions) error {
168166
if err != nil {
169167
return err
170168
}
171-
_, err = fmt.Fprintf(a.stdout, "Restored: %s\nCurrent backup: %s\n", target, currentBackup)
172-
return err
169+
return writeRestoreText(a.stdout, target, currentBackup)
173170
})
174171
}
175172

@@ -186,7 +183,8 @@ func (a App) version(ctx context.Context) error {
186183
func (a App) run(fn func() error) error {
187184
err := fn()
188185
if err != nil {
189-
_, _ = fmt.Fprintf(a.stderr, "error: %v\n", err)
186+
content := render.NewContent(a.stderr)
187+
content.Writef("error: %v\n", err)
190188
}
191189
return err
192190
}
@@ -293,23 +291,30 @@ func formatName(format history.Format) string {
293291
}
294292
}
295293

296-
func renderAnalyzeText(summary analyzeSummary) string {
297-
var b strings.Builder
298-
fmt.Fprintf(&b, "Scanned: %d\n", summary.Scanned)
299-
b.WriteString("Formats:\n")
294+
func writeAnalyzeText(w io.Writer, summary analyzeSummary) error {
295+
content := render.NewContent(w)
296+
content.Writef("Scanned: %d\n", summary.Scanned)
297+
content.WriteString("Formats:\n")
300298
for _, name := range []string{"zsh_extended", "plain", "malformed"} {
301-
fmt.Fprintf(&b, " %s: %d\n", name, summary.Formats[name])
299+
content.Writef(" %s: %d\n", name, summary.Formats[name])
302300
}
303-
fmt.Fprintf(&b, "Duplicate commands: %d\n", summary.DuplicateCommands)
304-
b.WriteString("Top commands:\n")
301+
content.Writef("Duplicate commands: %d\n", summary.DuplicateCommands)
302+
content.WriteString("Top commands:\n")
305303
if len(summary.TopCommands) == 0 {
306-
b.WriteString(" (none)\n")
307-
return b.String()
304+
content.WriteString(" (none)\n")
305+
return content.Err()
308306
}
309307
for _, top := range summary.TopCommands {
310-
fmt.Fprintf(&b, " %s: %d\n", top.Command, top.Count)
308+
content.Writef(" %s: %d\n", top.Command, top.Count)
311309
}
312-
return b.String()
310+
return content.Err()
311+
}
312+
313+
func writeRestoreText(w io.Writer, restoredPath, currentBackupPath string) error {
314+
content := render.NewContent(w)
315+
content.Writef("Restored: %s\n", restoredPath)
316+
content.Writef("Current backup: %s\n", currentBackupPath)
317+
return content.Err()
313318
}
314319

315320
func writeJSON(w io.Writer, payload any) error {

‎internal/render/content.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package render
2+
3+
import (
4+
"fmt"
5+
"io"
6+
)
7+
8+
type Content struct {
9+
writer io.Writer
10+
err error
11+
}
12+
13+
func NewContent(writer io.Writer) *Content {
14+
return &Content{writer: writer}
15+
}
16+
17+
func (c *Content) WriteString(text string) {
18+
if c.err != nil {
19+
return
20+
}
21+
_, c.err = io.WriteString(c.writer, text)
22+
}
23+
24+
func (c *Content) Writef(format string, args ...any) {
25+
if c.err != nil {
26+
return
27+
}
28+
_, c.err = fmt.Fprintf(c.writer, format, args...)
29+
}
30+
31+
func (c *Content) Err() error {
32+
return c.err
33+
}

‎internal/render/content_test.go‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package render
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestContentWritesFormattedText(t *testing.T) {
9+
writer := &recordingWriter{}
10+
content := NewContent(writer)
11+
12+
content.WriteString("Scanned: ")
13+
content.Writef("%d\n", 3)
14+
15+
if err := content.Err(); err != nil {
16+
t.Fatalf("Err() = %v, want nil", err)
17+
}
18+
if got, want := writer.text, "Scanned: 3\n"; got != want {
19+
t.Fatalf("content = %q, want %q", got, want)
20+
}
21+
}
22+
23+
func TestContentReturnsFirstWriteErrorAndStops(t *testing.T) {
24+
wantErr := errors.New("write failed")
25+
writer := &recordingWriter{err: wantErr}
26+
content := NewContent(writer)
27+
28+
content.Writef("first %s", "write")
29+
content.WriteString("second write")
30+
31+
if !errors.Is(content.Err(), wantErr) {
32+
t.Fatalf("Err() = %v, want %v", content.Err(), wantErr)
33+
}
34+
if got, want := writer.calls, 1; got != want {
35+
t.Fatalf("writer calls = %d, want %d", got, want)
36+
}
37+
}
38+
39+
type recordingWriter struct {
40+
text string
41+
err error
42+
calls int
43+
}
44+
45+
func (w *recordingWriter) Write(p []byte) (int, error) {
46+
w.calls++
47+
if w.err != nil {
48+
return 0, w.err
49+
}
50+
w.text += string(p)
51+
return len(p), nil
52+
}

‎internal/report/report_test.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package report
22

33
import (
44
"encoding/json"
5+
"errors"
56
"strings"
67
"testing"
78

@@ -112,6 +113,24 @@ func TestTextIncludesRemovedEntryDetails(t *testing.T) {
112113
}
113114
}
114115

116+
func TestWriteTextReturnsWriterError(t *testing.T) {
117+
wantErr := errors.New("write failed")
118+
119+
err := WriteText(failingWriter{err: wantErr}, Summary{Scanned: 1})
120+
121+
if !errors.Is(err, wantErr) {
122+
t.Fatalf("WriteText() error = %v, want %v", err, wantErr)
123+
}
124+
}
125+
126+
type failingWriter struct {
127+
err error
128+
}
129+
130+
func (w failingWriter) Write([]byte) (int, error) {
131+
return 0, w.err
132+
}
133+
115134
func TestJSONIncludesTotalsAndRemovedEntries(t *testing.T) {
116135
ts := int64(1710000000)
117136
summary := Summary{

‎internal/report/text.go‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package report
22

33
import (
4-
"fmt"
4+
"io"
55
"sort"
66
"strings"
77

88
"github.com/walker1211/histprune/internal/prune"
9+
"github.com/walker1211/histprune/internal/render"
910
)
1011

1112
// Summary is the report model shared by text, JSON, and app-level integration.
@@ -60,40 +61,46 @@ func FromDecisions(decisions []prune.Decision, dryRun bool, backupPath string) S
6061
// Text renders a compact human-readable report.
6162
func Text(summary Summary) string {
6263
var b strings.Builder
63-
fmt.Fprintf(&b, "Scanned: %d\n", summary.Scanned)
64-
fmt.Fprintf(&b, "Removed: %d\n", summary.Removed)
65-
fmt.Fprintf(&b, "Kept: %d\n", summary.Kept)
64+
_ = WriteText(&b, summary)
65+
return b.String()
66+
}
67+
68+
func WriteText(w io.Writer, summary Summary) error {
69+
content := render.NewContent(w)
70+
content.Writef("Scanned: %d\n", summary.Scanned)
71+
content.Writef("Removed: %d\n", summary.Removed)
72+
content.Writef("Kept: %d\n", summary.Kept)
6673
if summary.DryRun {
67-
b.WriteString("Mode: dry-run\n")
74+
content.WriteString("Mode: dry-run\n")
6875
} else {
69-
b.WriteString("Mode: write\n")
76+
content.WriteString("Mode: write\n")
7077
}
7178
if summary.BackupPath != "" {
72-
fmt.Fprintf(&b, "Backup: %s\n", summary.BackupPath)
79+
content.Writef("Backup: %s\n", summary.BackupPath)
7380
}
7481
if len(summary.RuleCounts) > 0 {
75-
b.WriteString("Rules:\n")
82+
content.WriteString("Rules:\n")
7683
keys := make([]string, 0, len(summary.RuleCounts))
7784
for key := range summary.RuleCounts {
7885
keys = append(keys, key)
7986
}
8087
sort.Strings(keys)
8188
for _, key := range keys {
82-
fmt.Fprintf(&b, " %s: %d\n", key, summary.RuleCounts[key])
89+
content.Writef(" %s: %d\n", key, summary.RuleCounts[key])
8390
}
8491
}
8592
if len(summary.RemovedEntries) > 0 {
86-
b.WriteString("Removed entries:\n")
93+
content.WriteString("Removed entries:\n")
8794
for _, entry := range summary.RemovedEntries {
88-
fmt.Fprintf(&b, " Line %d: %s\n", entry.LineNo, entry.Command)
95+
content.Writef(" Line %d: %s\n", entry.LineNo, entry.Command)
8996
for _, reason := range entry.Reasons {
9097
if reason.Detail == "" {
91-
fmt.Fprintf(&b, " - %s\n", reason.Type)
98+
content.Writef(" - %s\n", reason.Type)
9299
continue
93100
}
94-
fmt.Fprintf(&b, " - %s: %s\n", reason.Type, reason.Detail)
101+
content.Writef(" - %s: %s\n", reason.Type, reason.Detail)
95102
}
96103
}
97104
}
98-
return b.String()
105+
return content.Err()
99106
}

0 commit comments

Comments
 (0)