golang自定义 os.stderr 数据读取逻辑

发布时间 2023-08-08 21:35:38作者: ChnMig

原始需求

只是一个很简单的需求, 使用golang的exec运行一个命令然后获取实时结果, 命令是

trivy image --download-db-only

正常的打印应该是

2023-08-08T17:06:02.929+0800    INFO    Need to update DB
2023-08-08T17:06:02.929+0800    INFO    DB Repository: ghcr.io/aquasecurity/trivy-db
2023-08-08T17:06:02.929+0800    INFO    Downloading DB...
867.62 KiB / 38.79 MiB [->_____________________________________________________________________] 2.18% 804.52 KiB p/s ETA 48s

然后最下面是个进度条, 随着时间会慢慢的加满, 最后是到100结束

2023-08-08T17:36:33.103+0800    INFO    Need to update DB
2023-08-08T17:36:33.103+0800    INFO    DB Repository: ghcr.io/aquasecurity/trivy-db
2023-08-08T17:36:33.103+0800    INFO    Downloading DB...
38.79 MiB / 38.79 MiB [--------------------------------------------------------------------------] 100.00% 835.73 KiB p/s 48s

然后我就使用普通的 exec.Command 运行并监听其 stderr 和 stdout 通道, 结果发现进度条数据获取不到, 我将每一个数据都记录到了日志文件中, 日志文件如下:

{"level":"warn","ts":"2023-08-08T16:29:04.978+0800","caller":"trivy-db/download.go:46","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T16:29:04.978+0800\t\u001b[34mINFO\u001b[0m\tNeed to update DB"}

{"level":"warn","ts":"2023-08-08T16:29:04.978+0800","caller":"trivy-db/download.go:46","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T16:29:04.978+0800\t\u001b[34mINFO\u001b[0m\tDB Repository: ghcr.io/aquasecurity/trivy-db"}

{"level":"warn","ts":"2023-08-08T16:29:04.978+0800","caller":"trivy-db/download.go:46","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T16:29:04.978+0800\t\u001b[34mINFO\u001b[0m\tDownloading DB..."}

请忽略\t, [34m 这种特殊字符, 可以看到后续的进度条日志完全未捕捉到, 然后开始了找问题之旅, 因为觉得挺有意思所以把过程分享给大家 ?

确认代码

首先, 需要确认代码是不是有问题, 我的代码如下

var downloadDBCmd = []string{"trivy", "image", "--download-db-only", "--cache-dir", config.TrivyDBOriginPath}


// 返回DB的存储地址
func DownloadDB() (string, error) {
	// 创建Cmd对象
	cmd := exec.Command(downloadDBCmd[0], downloadDBCmd[1:]...)
	// 执行命令,并获取输出
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	outScanner := bufio.NewScanner(stdout)
	go func() {
		for outScanner.Scan() {
			zap.L().Info("命令正常输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(outScanner.Text()))
		}
	}()
	stderr, err := cmd.StderrPipe()
	if err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	errScanner := bufio.NewScanner(stderr)
	go func() {
		for errScanner.Scan() {
			zap.L().Warn("命令错误输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
		}
	}()
	if err := cmd.Start(); err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	if err := cmd.Wait(); err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	return config.TrivyDBOriginPath, nil
}

然后我更换了命令, 例如 ping www.baidu.com, apt update -y 等等, 发现, 当出现进度条的输出时, 程序会等待, 当程序正确结束后, 发现会在cmd结束时一起打印出来, 例如

16.00 KiB / 38.79 MiB [>_____________________________________________________________________________________________________] 0.04% ? p/s ?64.00 KiB / 38.79 MiB [>_____________________________________________________________________________________________________] 0.16% ? p/s ?176.00 KiB / 38.79 MiB [>____________________________________________________________________________________________________] 0.44% ? p/s ?304.00 KiB / 38.79 MiB [>___________________________________________________________________________________] 0.77% 480.27 KiB p/s ETA 1m22s496.00 KiB / 38.79 MiB [->__________________________________________________________________________________] 1.25% 480.27 KiB p/s ETA 1m21s496.00 KiB / 38.79 MiB [->__________________________________________________________________________________] 1.25% 480.27 KiB p/s ETA 1m21s496.00 KiB / 38.79 MiB [->__________________________________________________________________________________] 1.25% 469.91 KiB p/s ETA 1m23s688.00 KiB / 38.79 MiB [->__________________________________________________________________________________]

那么基本可以确定的是, 针对这种进度条在一行刷新的情况, 捕捉的时候会出现问题, 他不会将结果及时输出, 而之前 trivy 一直没输出, 大概率是因为网络问题导致进程一直卡住没有结束, 而后我通过手段对外网访问进行加速, 结果确实与我想象的一样, trivy 的进度条也会在结束的一瞬间全部打印出来, 为保险起见, 我还需要看一下 trivy 的代码, 确认一下trivy的打印实现.

查看 Trivy 代码

Trivy代码本身是开源的, 代码在 aquasecurity/trivy: Find vulnerabilities, misconfigurations, secrets, SBOM in containers, Kubernetes, code repositories, clouds and more (github.com)
我将其clone下来, 通过命令行参数一步步寻找到其--download-db-only 的处理部分, 在 trivy/pkg/commands/operation/operation.go at main · aquasecurity/trivy (github.com) 这里的 DownloadDB 函数, 随后又一步步的点击进逻辑 trivy/pkg/oci/artifact.go at main · aquasecurity/trivy (github.com) 这里的函数 download 中, 函数如下


func (a *Artifact) download(ctx context.Context, layer v1.Layer, fileName, dir string) error {
	size, err := layer.Size()
	if err != nil {
		return xerrors.Errorf("size error: %w", err)
	}

	rc, err := layer.Compressed()
	if err != nil {
		return xerrors.Errorf("failed to fetch the layer: %w", err)
	}
	defer rc.Close()

	// Show progress bar
	bar := pb.Full.Start64(size)
	if a.quiet {
		bar.SetWriter(io.Discard)
	}
	pr := bar.NewProxyReader(rc)
	defer bar.Finish()

	// https://github.com/hashicorp/go-getter/issues/326
	tempDir, err := os.MkdirTemp("", "trivy")
	if err != nil {
		return xerrors.Errorf("failed to create a temp dir: %w", err)
	}

	f, err := os.Create(filepath.Join(tempDir, fileName))
	if err != nil {
		return xerrors.Errorf("failed to create a temp file: %w", err)
	}
	defer func() {
		_ = f.Close()
		_ = os.RemoveAll(tempDir)
	}()

	// Download the layer content into a temporal file
	if _, err = io.Copy(f, pr); err != nil {
		return xerrors.Errorf("copy error: %w", err)
	}

	// Decompress the downloaded file if it is compressed and copy it into the dst
	if err = downloader.Download(ctx, f.Name(), dir, dir); err != nil {
		return xerrors.Errorf("download error: %w", err)
	}

	return nil
}

其中, 代码段的注释提醒我这里是进度条的展示

// Show progress bar
bar := pb.Full.Start64(size)
if a.quiet {
	bar.SetWriter(io.Discard)
}
pr := bar.NewProxyReader(rc)
defer bar.Finish()

然后看了一下pb 发现是一个第三方的模块, 专门进行进度条的展示
cheggaaa/pb: Console progress bar for Golang (github.com)
然后又去 trivy 的 go.mod 中确认其使用的是最新的v3版本

github.com/cheggaaa/pb/v3 v3.1.2

而后, 我查看了pb项目的文档, 发现 SetWriter 函数来设置进度条输出的地方 cheggaaa/pb: Console progress bar for Golang (github.com)
我也查看了该函数的注释,确实如此, 并且默认情况下是输出到 stderr

// SetWriter sets the io.Writer. Bar will write in this writer
// By default this is os.Stderr
func (pb *ProgressBar) SetWriter(w io.Writer) *ProgressBar {
	pb.mu.Lock()
	pb.output = w
	pb.configured = false
	pb.configure()
	pb.mu.Unlock()
	return pb
}

我注意到, trivy这里的代码有一个分支处理, 如果 a.quiettrue 则设置输出通道为 io.Discard, 而io.Discard 是一个不会在任何地方打印的通道, 所以我要确认什么时候 a.quietTrue
后来, 我一步步通过代码发现, 只有在携带参数 --quiet 时, a.quiet才为真, 这点也在 help 里得到了证实

Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues and hard-coded secrets

Usage:
  trivy [global flags] command [flags] target
  trivy [command]

Examples:
  # Scan a container image
  $ trivy image python:3.4-alpine

  # Scan a container image from a tar archive
  $ trivy image --input ruby-3.1.tar

  # Scan local filesystem
  $ trivy fs .

  # Run in server mode
  $ trivy server

Scanning Commands
  aws         [EXPERIMENTAL] Scan AWS account
  config      Scan config files for misconfigurations
  filesystem  Scan local filesystem
  image       Scan a container image
  kubernetes  [EXPERIMENTAL] Scan kubernetes cluster
  repository  Scan a remote repository
  rootfs      Scan rootfs
  sbom        Scan SBOM for vulnerabilities
  vm          [EXPERIMENTAL] Scan a virtual machine image

Management Commands
  module      Manage modules
  plugin      Manage plugins

Utility Commands
  completion  Generate the autocompletion script for the specified shell
  convert     Convert Trivy JSON report into a different format
  help        Help about any command
  server      Server mode
  version     Print the version

Flags:
      --cache-dir string          cache directory (default "/root/.cache/trivy")
  -c, --config string             config path (default "trivy.yaml")
  -d, --debug                     debug mode
  -f, --format string             version format (json)
      --generate-default-config   write the default config to trivy-default.yaml
  -h, --help                      help for trivy
      --insecure                  allow insecure server connections
  -q, --quiet                     suppress progress bar and log output
      --timeout duration          timeout (default 5m0s)
  -v, --version                   show version

Use "trivy [command] --help" for more information about a command.

当然我也加上了 --quiet 进行了测试发现加上参数后才是真正的不输出.
到目前为止, 我确认了, 默认情况下还是会输入到os.stderr 中, 那么我现在需要考虑的是, 为什么他在结束的全部打印, 从最后的全部打印来看, 每一行数据中间并没有 \n 分隔, 那么我第一时间认为, 是不是因为捕捉到的结果都是堆到一起的, 而因为中间的分隔符错误, 导致程序认为这是一行输出, 还没有结束, 一直积攒起来, 等到程序结束了, 一起打印出来呢?

stderr监听方式

我查看了我的stderr监听代码, 主要这一段

stderr, err := cmd.StderrPipe()
if err != nil {
	zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
	return "", err
}
errScanner := bufio.NewScanner(stderr)
errScanner.Split(ScanLines)
go func() {
	for errScanner.Scan() {
		zap.L().Warn("命令错误输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
	}
}()

我通过查看这些函数内部代码, 发现 bufio.NewScanner 返回的对象中有一个split 属性, 默认是 ScanLines 这个函数

// NewScanner returns a new Scanner to read from r.
// The split function defaults to ScanLines.
func NewScanner(r io.Reader) *Scanner {
	return &Scanner{
		r:            r,
		split:        ScanLines,
		maxTokenSize: MaxScanTokenSize,
	}
}

我发现这个 split 属性是一个函数, 函数内部定义了怎么将studerr内的数据分割和输出

// ScanLines is a split function for a Scanner that returns each line of
// text, stripped of any trailing end-of-line marker. The returned line may
// be empty. The end-of-line marker is one optional carriage return followed
// by one mandatory newline. In regular expression notation, it is `\r?\n`.
// The last non-empty line of input will be returned even if it has no
// newline.
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
		// We have a full newline-terminated line.
		return i + 1, dropCR(data[0:i]), nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), dropCR(data), nil
	}
	// Request more data.
	return 0, nil, nil
}

从代码里可以清晰看出, 这里是根据data的\n 进行分割, 如果没有\n, 并且没有EOF结束, 则返回0, 等待下次有数据来再进行这个函数的调用.
而之前的 Trivy 运行, 因为网络问题, 虽然有打印输出到 stderr, 进入了这个ScanLines 函数, 但是因为没有\n, 导致一直没有发送到scanner中
而后来进行了网络加速, 可以正常下载了, 于是正常触发了EOF将所有data一并打出
那么下一步, 想办法能不能把这个函数给替换成我自定义的

自定义split逻辑

我查看了 Scanner 的其他方法, 果然发现了一个 Split 方法

// Split sets the split function for the Scanner.
// The default split function is ScanLines.
//
// Split panics if it is called after scanning has started.
func (s *Scanner) Split(split SplitFunc) {
	if s.scanCalled {
		panic("Split called after Scan")
	}
	s.split = split
}

看注释, 可以替换默认的分割器, 参数 split 类型是一个函数

// SplitFunc is the signature of the split function used to tokenize the
// input. The arguments are an initial substring of the remaining unprocessed
// data and a flag, atEOF, that reports whether the Reader has no more data
// to give. The return values are the number of bytes to advance the input
// and the next token to return to the user, if any, plus an error, if any.
//
// Scanning stops if the function returns an error, in which case some of
// the input may be discarded. If that error is ErrFinalToken, scanning
// stops with no error.
//
// Otherwise, the Scanner advances the input. If the token is not nil,
// the Scanner returns it to the user. If the token is nil, the
// Scanner reads more data and continues scanning; if there is no more
// data--if atEOF was true--the Scanner returns. If the data does not
// yet hold a complete token, for instance if it has no newline while
// scanning lines, a SplitFunc can return (0, nil, nil) to signal the
// Scanner to read more data into the slice and try again with a
// longer slice starting at the same point in the input.
//
// The function is never called with an empty data slice unless atEOF
// is true. If atEOF is true, however, data may be non-empty and,
// as always, holds unprocessed text.
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

于是我进行测试, 直接将默认的ScanLines 抄下来, 改成检测到 空格 就分割抛出, 看看效果


// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
	if len(data) > 0 && data[len(data)-1] == '\r' {
		return data[0 : len(data)-1]
	}
	return data
}

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	// 检测到空格就抛出
	if i := bytes.IndexByte(data, ' '); i >= 0 {
		// We have a full newline-terminated line.
		return i + 1, dropCR(data[0:i]), nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), dropCR(data), nil
	}
	// Request more data.
	return 0, nil, nil
}

// 返回DB的存储地址
func DownloadDB() (string, error) {
	// 创建Cmd对象
	cmd := exec.Command(downloadDBCmd[0], downloadDBCmd[1:]...)
	// 执行命令,并获取输出
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	outScanner := bufio.NewScanner(stdout)
	go func() {
		for outScanner.Scan() {
			zap.L().Info("命令正常输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(outScanner.Text()))
		}
	}()
	stderr, err := cmd.StderrPipe()
	if err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	errScanner := bufio.NewScanner(stderr)
	errScanner.Split(ScanLines)  // 替换默认的分割器
	go func() {
		for errScanner.Scan() {
			zap.L().Warn("命令错误输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
		}
	}()
	if err := cmd.Start(); err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	if err := cmd.Wait(); err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	return config.TrivyDBOriginPath, nil
}

经过运行, 发现果然是立刻就记录了, 当然是按照空格区分肯定是不满足要求的

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"DB...\n32.00"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"KiB"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"/"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"38.79"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only","output":"MiB"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"[>_____________________________________________________________________________________________________]"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"0.08%"}

{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"?"}

于是我分析了真正每行的数据, 然后通过 errScanner.Bytes 打印原有的 bytes 值, 期望能找到隐藏的分割每次输出的标识, 很遗憾, 并没有.
而后, 我只能选择根据一定的特征, 去做切分, 毕竟我的需求是日志中能看到进度就行, 我注意到, 每行的数据的后面一定包含字符串 p/s ,其他是会变得, 只有这个是永远存在的, 可以考虑按照字符串 p/s 分割切分
于是将代码修改成按照p/s 切割

// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
	if len(data) > 0 && data[len(data)-1] == '\r' {
		return data[0 : len(data)-1]
	}
	return data
}

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	if i := strings.Index(string(data), "p/s"); i >= 0 {
		// We have a full newline-terminated line.
		return i + len("p/s") + 1, dropCR(data[0 : i+len("p/s")]), nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), dropCR(data), nil
	}
	// Request more data.
	return 0, nil, nil
}

查看运行结果

{"level":"warn","ts":"2023-08-08T19:41:54.404+0800","caller":"trivy-db/download.go:65","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"ETA 54s2.45 MiB / 38.80 MiB [---->____________________________________________________________________] 6.31% 681.84 KiB p/s"}

因为按照 p/s 切分, 所以原本后面的 ETA xx 会放到下一行最前面, 但是也可以解决查看不了进度的问题
后来我又突发奇想, 进度条是每次都完整输出一行到data, 触发一次切割器, 能不能通过在触发器里直接将这个data所有都传出来的方式呢? 于是我将自定义切割器直接修改成

// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
	if len(data) > 0 && data[len(data)-1] == '\r' {
		return data[0 : len(data)-1]
	}
	return data
}

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	return len(data), dropCR(data), nil
}

运行结果果然如我所料, 只是进度条之前打印的基础信息, 因为是 fmt.Printf 一次出来的, 所以不会分割

{"level":"warn","ts":"2023-08-08T19:32:51.680+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T19:32:51.680+0800\t\u001b[34mINFO\u001b[0m\tNeed to update DB\n2023-08-08T19:32:51.680+0800\t\u001b[34mINFO\u001b[0m\tDB Repository: ghcr.io/aquasecurity/trivy-db\n2023-08-08T19:32:51.680+0800\t\u001b[34mINFO\u001b[0m\tDownloading DB...\n"}
{"level":"warn","ts":"2023-08-08T19:32:54.683+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"16.00 KiB / 38.80 MiB [>______________________________________________________________________________________] 0.04% ? p/s ?"}
{"level":"warn","ts":"2023-08-08T19:32:54.881+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"96.00 KiB / 38.80 MiB [>______________________________________________________________________________________] 0.24% ? p/s ?"}
{"level":"warn","ts":"2023-08-08T19:32:55.082+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"240.00 KiB / 38.80 MiB [>_____________________________________________________________________________________] 0.60% ? p/s ?"}
{"level":"warn","ts":"2023-08-08T19:32:55.284+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"496.00 KiB / 38.80 MiB [>______________________________________________________________________] 1.25% 797.25 KiB p/s ETA 49s"}

可以看到这样的效果更加的好评

最终代码

贴一下最终的代码

package trivydb

import (
	"bufio"
	"os/exec"

	"go.uber.org/zap"

	"hids-v3-container-manager/config"
	"hids-v3-container-manager/utils/log"
)

var downloadDBCmd = []string{"trivy", "image", "--download-db-only", "--cache-dir", config.TrivyDBOriginPath}

// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
	if len(data) > 0 && data[len(data)-1] == '\r' {
		return data[0 : len(data)-1]
	}
	return data
}

// 适配进度条
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	return len(data), dropCR(data), nil
}

// 返回DB的存储地址
func DownloadDB() (string, error) {
	// 创建Cmd对象
	cmd := exec.Command(downloadDBCmd[0], downloadDBCmd[1:]...)
	// 执行命令,并获取输出
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	outScanner := bufio.NewScanner(stdout)
	go func() {
		for outScanner.Scan() {
			zap.L().Info("命令输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(outScanner.Text()))
		}
	}()
	stderr, err := cmd.StderrPipe()
	if err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	errScanner := bufio.NewScanner(stderr)
	errScanner.Split(scanLines) // 自定义切割
	go func() {
		for errScanner.Scan() {
			zap.L().Info("命令输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
		}
	}()
	if err := cmd.Start(); err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	if err := cmd.Wait(); err != nil {
		zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
		return "", err
	}
	return config.TrivyDBOriginPath, nil
}

收获

  • 对 Trivy 代码更加了解
  • 知道进度条模块pb基本使用
  • 了解了 os.stderr os.stdout 的处理逻辑
  • 了解了如何自定义切割器实现自己想要的效果
    ps: 除了默认切割器scanLines, 他还自带了一写其他的, 比如按照bytes直接切分的 ScanBytes 等等, 读者有兴趣也可以看一下