From 86d62039a9e8e934e8b67cbc39701a308c5ea9d9 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Wed, 30 Jul 2025 14:42:07 +0200 Subject: [PATCH] Read STDIN in catp, optimize PrepareLine --- .github/workflows/release-assets.yml | 2 +- .github/workflows/test-unit.yml | 5 +- cmd/catp/README.md | 3 +- cmd/catp/catp/app.go | 150 +++++++++++++++++++-------- cmd/catp/default.pgo | Bin 12938 -> 6611 bytes go.mod | 4 +- go.sum | 8 +- 7 files changed, 119 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index 8282f46..6dc262d 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -14,7 +14,7 @@ env: jobs: build: name: Upload Release Assets - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Install Go uses: actions/setup-go@v5 diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index a91bfee..978f679 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -80,8 +80,9 @@ jobs: curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.4.2/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && rm linux_amd64.tar.gz gocovdiff_hash=$(git hash-object ./gocovdiff) [ "$gocovdiff_hash" == "c37862c73a677e5a9c069470287823ab5bbf0244" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1) - git fetch origin master ${{ github.event.pull_request.base.sha }} - REP=$(./gocovdiff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) + # Fetch PR diff from GitHub API. + curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github.v3.diff" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }} > pull_request.diff + REP=$(./gocovdiff -diff pull_request.diff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) echo "${REP}" cat gha-unit.txt DIFF=$(test -e unit-base.txt && ./gocovdiff -mod github.com/$GITHUB_REPOSITORY -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file") diff --git a/cmd/catp/README.md b/cmd/catp/README.md index 5bbbac9..82cf9d0 100644 --- a/cmd/catp/README.md +++ b/cmd/catp/README.md @@ -23,6 +23,7 @@ catp dev, go1.22rc1 CGO_ZSTD catp prints contents of files to STDOUT or dir/file output, while printing current progress status to STDERR. It can decompress data from .gz and .zst files. +Use dash (-) as PATH to read STDIN. Usage of catp: catp [OPTIONS] PATH ... @@ -37,7 +38,7 @@ catp [OPTIONS] PATH ... files will be written to out dir with original base names disables output flag -output string - output to file instead of STDOUT + output to file (can have .gz or .zst ext for compression) instead of STDOUT -parallel int number of parallel readers if multiple files are provided lines from different files will go to output simultaneously (out of order of files, but in order of lines in each file) diff --git a/cmd/catp/catp/app.go b/cmd/catp/catp/app.go index f9b44c3..ca9b2f7 100644 --- a/cmd/catp/catp/app.go +++ b/cmd/catp/catp/app.go @@ -61,6 +61,32 @@ type runner struct { hasOptions bool options Options + + hasCompression bool +} + +// humanReadableBytes converts bytes to a human-readable string (TB, GB, MB, KB, or bytes). +func humanReadableBytes(bytes int64) string { + const ( + Byte = 1 + KByte = Byte * 1024 + MByte = KByte * 1024 + GByte = MByte * 1024 + TByte = GByte * 1024 + ) + + switch { + case bytes >= TByte: + return fmt.Sprintf("%.2f TB", float64(bytes)/float64(TByte)) + case bytes >= GByte: + return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GByte)) + case bytes >= MByte: + return fmt.Sprintf("%.2f MB", float64(bytes)/float64(MByte)) + case bytes >= KByte: + return fmt.Sprintf("%.2f KB", float64(bytes)/float64(KByte)) + default: + return fmt.Sprintf("%d B", bytes) + } } // st renders Status as a string. @@ -98,17 +124,29 @@ func (r *runner) st(s progress.Status) string { } } else { if s.LinesCompleted != 0 { - res = fmt.Sprintf("%s: %.1f%% bytes read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s, remaining %s", - s.Task, s.DonePercent, s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS, - s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String()) + if r.totalBytes == -1 { // STDIN + res = fmt.Sprintf("%s read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s", + humanReadableBytes(s.BytesCompleted), s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS, + s.Elapsed.Round(10*time.Millisecond).String()) + } else { + res = fmt.Sprintf("%s: %.1f%% bytes read, %d lines processed, %.1f l/s, %.1f MB/s, elapsed %s, remaining %s", + s.Task, s.DonePercent, s.LinesCompleted, s.SpeedLPS, s.SpeedMBPS, + s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String()) + } } else { - res = fmt.Sprintf("%s: %.1f%% bytes read, %.1f MB/s, elapsed %s, remaining %s", - s.Task, s.DonePercent, s.SpeedMBPS, - s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String()) + if r.totalBytes == -1 { + res = fmt.Sprintf("%s read, %.1f MB/s, elapsed %s", + humanReadableBytes(s.BytesCompleted), s.SpeedMBPS, + s.Elapsed.Round(10*time.Millisecond).String()) + } else { + res = fmt.Sprintf("%s: %.1f%% bytes read, %.1f MB/s, elapsed %s, remaining %s", + s.Task, s.DonePercent, s.SpeedMBPS, + s.Elapsed.Round(10*time.Millisecond).String(), s.Remaining.String()) + } } } - if currentBytesUncompressed > currentBytes { + if currentBytesUncompressed > currentBytes && r.hasCompression { lastBytesUncompressed := atomic.LoadInt64(&r.lastBytesUncompressed) lastStatusTime := atomic.LoadInt64(&r.lastStatusTime) now := time.Now().Unix() @@ -158,6 +196,7 @@ func (r *runner) scanFile(filename string, rd io.Reader, out io.Writer) { s.Buffer(make([]byte, 64*1024), 10*1024*1024) lines := 0 + buf := make([]byte, 64*1024) for s.Scan() { lines++ @@ -175,7 +214,8 @@ func (r *runner) scanFile(filename string, rd io.Reader, out io.Writer) { if r.hasOptions { if r.options.PrepareLine != nil { - line = r.options.PrepareLine(filename, lines, line) + buf = buf[:0] + line = r.options.PrepareLine(filename, lines, line, &buf) } if line == nil { @@ -265,26 +305,32 @@ func (r *runner) shouldWrite(line []byte) bool { } func (r *runner) cat(filename string) (err error) { //nolint:gocyclo - file, err := os.Open(filename) //nolint:gosec - if err != nil { - return err - } + var rd io.Reader - defer func() { - if clErr := file.Close(); clErr != nil && err == nil { - err = clErr + if filename == "-" { + rd = os.Stdin + } else { + file, err := os.Open(filename) //nolint:gosec + if err != nil { + return err } - }() - rd := io.Reader(file) + defer func() { + if clErr := file.Close(); clErr != nil && err == nil { + err = clErr + } + }() + + rd = io.Reader(file) + } if !r.noProgress { - cr := progress.NewCountingReader(file) + cr := progress.NewCountingReader(rd) cr.SetBytes(&r.currentBytes) cr.SetLines(nil) if r.parallel <= 1 { - cr = progress.NewCountingReader(file) + cr = progress.NewCountingReader(rd) cr.SetLines(nil) r.currentFile = cr r.currentTotal = r.sizes[filename] @@ -425,7 +471,8 @@ func (i *stringFlags) Set(value string) error { // Options allows behavior customisations. type Options struct { // PrepareLine is invoked for every line, if result is nil, line is skipped. - PrepareLine func(filename string, lineNr int, line []byte) []byte + // You can use buf to avoid allocations for a result, and change its capacity if needed. + PrepareLine func(filename string, lineNr int, line []byte, buf *[]byte) []byte } // Main is the entry point for catp CLI tool. @@ -437,14 +484,6 @@ func Main(options ...func(o *Options)) error { //nolint:funlen,cyclop,gocognit,g r := &runner{} - if len(options) > 0 { - r.hasOptions = true - - for _, opt := range options { - opt(&r.options) - } - } - flag.Var(&pass, "pass", "filter matching, may contain multiple AND patterns separated by ^,\n"+ "if filter matches, line is passed to the output (unless filtered out by -skip)\n"+ "each -pass value is added with OR logic,\n"+ @@ -476,7 +515,8 @@ func Main(options ...func(o *Options)) error { //nolint:funlen,cyclop,gocognit,g fmt.Println() fmt.Println("catp prints contents of files to STDOUT or dir/file output, \n" + "while printing current progress status to STDERR. \n" + - "It can decompress data from .gz and .zst files.") + "It can decompress data from .gz and .zst files.\n" + + "Use dash (-) as PATH to read STDIN.") fmt.Println() fmt.Println("Usage of catp:") fmt.Println("catp [OPTIONS] PATH ...") @@ -502,6 +542,14 @@ func Main(options ...func(o *Options)) error { //nolint:funlen,cyclop,gocognit,g defer pprof.StopCPUProfile() } + if len(options) > 0 { + r.hasOptions = true + + for _, opt := range options { + opt(&r.options) + } + } + if *output != "" && r.outDir == "" { //nolint:nestif fn := *output @@ -597,35 +645,51 @@ func Main(options ...func(o *Options)) error { //nolint:funlen,cyclop,gocognit,g var files []string - for _, f := range flag.Args() { - glob, err := filepath.Glob(f) - if err != nil { - return err - } + args := flag.Args() - for _, f := range glob { - alreadyThere := false + if len(args) == 1 && args[0] == "-" { + files = append(files, "-") // STDIN + } else { + for _, f := range args { + glob, err := filepath.Glob(f) + if err != nil { + return err + } - for _, e := range files { - if e == f { - alreadyThere = true + for _, f := range glob { + alreadyThere := false - break + for _, e := range files { + if e == f { + alreadyThere = true + + break + } } - } - if !alreadyThere { - files = append(files, f) + if !alreadyThere { + files = append(files, f) + } } } } for _, fn := range files { + if fn == "-" { + r.totalBytes = -1 + + continue + } + st, err := os.Stat(fn) if err != nil { return fmt.Errorf("failed to read file stats %s: %w", fn, err) } + if strings.HasSuffix(fn, ".zst") || strings.HasSuffix(fn, ".gz") { + r.hasCompression = true + } + r.totalBytes += st.Size() r.sizes[fn] = st.Size() } diff --git a/cmd/catp/default.pgo b/cmd/catp/default.pgo index 4d2e9ebb7844ca75e2b06cc748b556de63ee6a3d..9f053fe5bfd15d2214f4edfedbb2e462cac38056 100644 GIT binary patch literal 6611 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 R{{R30|Nkv195HM|000JmqoDu* literal 12938 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