
本文介绍一种健壮、简洁的方式,使用 bufio.Scanner 替代手动实现的缓冲逻辑,来实时过滤命令输出中的 \r 终止行(如进度条),仅保留每组连续 \r 行的首尾有效内容。
本文介绍一种健壮、简洁的方式,使用 `bufio.scanner` 替代手动实现的缓冲逻辑,来实时过滤命令输出中的 `\r` 终止行(如进度条),仅保留每组连续 `\r` 行的首尾有效内容。
在 Go 中对标准输出(stdout)进行流式字节过滤时,核心挑战在于:输入是分块到达的(如 Write() 调用),而语义单位(如“一行”)可能被截断跨块。原代码中 myFilter 手动管理 partialLine 的逻辑存在边界缺陷——例如未正确处理 \r 行末无 \n、多 \r 连续出现、或 \r 与 \n 混合(如 \r\n)等场景,导致解析错位(如 fromCmd 输出中出现 ss: 100% ——明显是 progress: 100% 被错误切分的结果)。
更可靠的做法是委托给标准库的成熟组件:bufio.Scanner。它专为按行读取设计,自动处理跨 Write() 调用的缓冲、换行符识别(支持 \n、\r\n、\r 等)、内存复用,并保证每次 Scan() 返回完整逻辑行(不含换行符)。我们只需在其基础上添加 \r 分割逻辑即可。
以下是推荐实现:
package main
import (
"bytes"
"fmt"
"os/exec"
"bufio"
"io"
)
func filterCRStream(in io.Reader) ([]byte, error) {
var out bytes.Buffer
scanner := bufio.NewScanner(in)
// 可选:提升大行性能(默认 64KB 限制)
scanner.Buffer(make([]byte, 4096), 1 1 {
last := bytes.TrimSpace(segments[len(segments)-1])
// 防止与第一段相同(如 "100%\r100%" → 两次 "100%")
if !bytes.Equal(last, segments[0]) && len(last) > 0 {
out.Write(last)
out.WriteString("\n")
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan error: %w", err)
}
return out.Bytes(), nil
}
func main() {
// 示例:模拟命令输出
cmd := exec.Command("bash", "-c",
`printf "a\nb\n\nprogress: 98%\r";
printf "progress: 99%\r";
printf "progress: 100%\r";
printf "\n\nc\n"`)
stdout, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
if err := cmd.Start(); err != nil {
panic(err)
}
filtered, err := filterCRStream(stdout)
if err != nil {
panic(err)
}
if err := cmd.Wait(); err != nil {
panic(err)
}
fmt.Printf("Filtered output:\n%s", filtered)
}
关键优势说明:
- ✅ 自动缓冲:
Scanner内部维护缓冲区,无需手动拼接partialLine; - ✅ 换行鲁棒性:默认识别
\n、\r\n,亦可自定义SplitFunc处理纯\r; - ✅ 内存安全:
scanner.Bytes()返回的切片指向内部缓冲,需立即拷贝(本例中out.Write()已隐式拷贝); - ✅ 错误传播:
scanner.Err()显式暴露 I/O 错误,便于调试; - ⚠️ 注意:若原始流含二进制数据或非法 UTF-8,
Scanner仍适用(它不校验文本编码),但Bytes()返回原始字节,完全满足字节流过滤需求。
总结:面对流式字节过滤任务,优先复用 bufio.Scanner 或 io.Copy + 自定义 io.Writer,而非从零实现状态机。这不仅减少 bug(如原文本中因 lines[0] 和 lines[last] 边界判断失误导致的截断),也显著提升代码可维护性与可测试性。











