
本文介绍一种健壮、简洁且符合 Go 习惯的方式,使用 bufio.Scanner 实时过滤命令输出中的 \r 终止行(如进度条),仅保留每组连续 \r 行的首尾有效内容,避免手动管理部分行(partial line)导致的逻辑错误。
本文介绍一种健壮、简洁且符合 go 习惯的方式,使用 `bufio.scanner` 实时过滤命令输出中的 `\r` 终止行(如进度条),仅保留每组连续 `\r` 行的首尾有效内容,避免手动管理部分行(partial line)导致的逻辑错误。
在 Go 中对标准输出(stdout)进行流式字节过滤——尤其是处理类似 curl 或 rsync 输出中频繁覆盖的 \r 进度指示器——关键在于:既要保证实时性,又要正确切分逻辑行。原始实现中手动维护 partialLine 并拆分 \n 的方式极易出错:它假设输入块边界与行边界对齐,而实际系统调用(如 exec.Command 的管道读取)返回的 []byte 可能在任意位置截断(例如 \r 后半截、\n 被劈开),导致 bytes.SplitAfter(p, lf) 行为不可靠,进而引发 lines[0] 错误拼接、cr 判断失效等问题——这正是 fromCmd 输出出现 ss: 100%(progess: 100% 被截断)和重复 progess: 100% 的根本原因。
更优解是交由 Go 标准库的 bufio.Scanner 处理行缓冲。它内部自动累积未完成的行(partial line),仅在遇到完整 \n(或自定义分隔符)时触发 Scan(),从而彻底规避手动状态管理的复杂性。以下为推荐实现:
package main
import (
"bufio"
"bytes"
"fmt"
"os/exec"
)
func filterProgressStream(in []byte) []byte {
var out bytes.Buffer
scanner := bufio.NewScanner(bytes.NewReader(in))
// 使用默认 SplitFunc(按 \n 分割),确保每轮 Scan() 返回完整逻辑行
for scanner.Scan() {
line := scanner.Bytes()
// 将当前行按 \r 拆分:每个 \r 视为一次覆盖,只保留首次(lines[0])和最终覆盖内容(lines[len(lines)-1])
parts := bytes.Split(line, []byte("\r"))
if len(parts) == 0 {
continue
}
// 写入首段(初始状态)
out.Write(bytes.TrimSpace(parts[0]))
out.WriteString("\n")
// 若存在多次覆盖,写入末段(最终状态)
if len(parts) > 1 {
// 注意:parts[len(parts)-1] 可能为空(如 "foo\r" → ["foo", ""]),跳过空行
if len(parts[len(parts)-1]) > 0 {
out.Write(bytes.TrimSpace(parts[len(parts)-1]))
out.WriteString("\n")
}
}
}
return out.Bytes()
}
// 示例:模拟命令输出过滤
func main() {
// 构造含 \r 进度条的测试数据
input := []byte("a\nb\n\nprogress: 98%\rprogress: 99%\rprogress: 100%\r\n\nc\n")
filtered := filterProgressStream(input)
fmt.Printf("Filtered output:\n%s", filtered)
// 实际命令使用示例:
// cmd := exec.Command("your-command")
// stdout, _ := cmd.StdoutPipe()
// go func() {
// io.Copy(&outBuffer, bufio.NewReader(stdout)) // 或直接 scanner 扫描
// }()
// cmd.Start()
// cmd.Wait()
}
✅ 优势总结:
-
零状态管理:
bufio.Scanner自动处理跨 chunk 的\n边界,无需partialLine; -
语义清晰:每轮
Scan()对应一个\n终止的逻辑行,bytes.Split(..., \r)在该行内操作,逻辑封闭、可预测; -
安全鲁棒:显式检查
parts[len(parts)-1]非空,避免写入空行;bytes.TrimSpace消除\r残留空白; -
易于扩展:如需保留中间状态或添加超时丢弃,可在
for scanner.Scan()循环内灵活插入逻辑。
⚠️ 注意事项:
-
bufio.Scanner默认最大行长度为 64KB,若预期有超长行(罕见),需调用scanner.Buffer(make([]byte, 64*1024), 1 调整; - 此方案按
\n划分“进度组”,即A\rB\rC\n视为一组,输出A和C;若需跨\n的全局进度聚合(如多行滚动日志),需改用自定义SplitFunc或bufio.Reader.ReadBytes手动解析; - 实际集成到
exec.Command时,建议将stdout接入bufio.Scanner(而非直接赋值给自定义Write结构体),以保持流式处理一致性。
采用 bufio.Scanner 不仅修复了原始代码的边界缺陷,更让过滤逻辑回归 Go 的惯用范式:用组合代替手写状态机,用标准工具代替脆弱的字节切片逻辑。











