@@ -37,9 +37,11 @@ type runner struct {
3737 totalBytes int64
3838 outDir string
3939
40- parallel int
41- currentBytes int64
42- currentLines int64
40+ parallel int
41+
42+ currentBytes int64
43+ currentBytesUncompressed int64
44+ currentLines int64
4345
4446 // pass is a slice of OR items, that are slices of AND items.
4547 pass [][][]byte
@@ -49,7 +51,9 @@ type runner struct {
4951 currentFile * progress.CountingReader
5052 currentTotal int64
5153
52- lastErr error
54+ lastErr error
55+ lastStatusTime int64
56+ lastBytesUncompressed int64
5357}
5458
5559// st renders Status as a string.
@@ -70,6 +74,9 @@ func (r *runner) st(s progress.Status) string {
7074 Status : s ,
7175 }
7276
77+ currentBytesUncompressed := atomic .LoadInt64 (& r .currentBytesUncompressed )
78+ currentBytes := atomic .LoadInt64 (& r .currentBytes )
79+
7380 if len (r .sizes ) > 1 && r .parallel <= 1 {
7481 pr .CurrentFilePercent = 100 * float64 (r .currentFile .Bytes ()) / float64 (r .currentTotal )
7582
@@ -82,6 +89,20 @@ func (r *runner) st(s progress.Status) string {
8289 s .Elapsed .Round (10 * time .Millisecond ).String (), s .Remaining .String ())
8390 }
8491
92+ if currentBytesUncompressed > currentBytes {
93+ lastBytesUncompressed := atomic .LoadInt64 (& r .lastBytesUncompressed )
94+ lastStatusTime := atomic .LoadInt64 (& r .lastStatusTime )
95+ now := time .Now ().Unix ()
96+
97+ if lastBytesUncompressed != 0 && lastStatusTime != 0 && lastStatusTime != now {
98+ spdMPBS := (float64 (currentBytesUncompressed - lastBytesUncompressed ) / float64 (now - lastStatusTime )) / (1024 * 1024 )
99+ res = strings .ReplaceAll (res , "MB/s" , fmt .Sprintf ("MB/s (uncomp %.1f MB/s)" , spdMPBS ))
100+ }
101+
102+ atomic .StoreInt64 (& r .lastStatusTime , now )
103+ atomic .StoreInt64 (& r .lastBytesUncompressed , currentBytesUncompressed )
104+ }
105+
85106 if len (r .pass ) > 0 || len (r .skip ) > 0 {
86107 m := atomic .LoadInt64 (& r .matches )
87108 pr .Matches = & m
@@ -91,7 +112,7 @@ func (r *runner) st(s progress.Status) string {
91112 if r .progressJSON != "" {
92113 pr .ElapsedSeconds = pr .Elapsed .Truncate (time .Second ).Seconds ()
93114 pr .RemainingSeconds = pr .Remaining .Round (time .Second ).Seconds ()
94- pr .BytesCompleted = atomic . LoadInt64 ( & r . currentBytes )
115+ pr .BytesCompleted = currentBytes
95116 pr .BytesTotal = atomic .LoadInt64 (& r .totalBytes )
96117
97118 if j , err := json .Marshal (pr ); err == nil {
@@ -241,6 +262,13 @@ func (r *runner) cat(filename string) (err error) {
241262 return err
242263 }
243264
265+ crl := progress .NewCountingReader (rd )
266+
267+ crl .SetBytes (& r .currentBytesUncompressed )
268+ crl .SetLines (& r .currentLines )
269+
270+ rd = crl
271+
244272 out := r .output
245273
246274 if r .outDir != "" {
@@ -358,7 +386,7 @@ func (i *stringFlags) Set(value string) error {
358386}
359387
360388// Main is the entry point for catp CLI tool.
361- func Main () error { //nolint:funlen,cyclop,gocognit,gocyclo
389+ func Main () error { //nolint:funlen,cyclop,gocognit,gocyclo,maintidx
362390 var (
363391 pass stringFlags
364392 skip stringFlags
@@ -382,7 +410,7 @@ func Main() error { //nolint:funlen,cyclop,gocognit,gocyclo
382410
383411 cpuProfile := flag .String ("dbg-cpu-prof" , "" , "write first 10 seconds of CPU profile to file" )
384412 memProfile := flag .String ("dbg-mem-prof" , "" , "write heap profile to file after 10 seconds" )
385- output := flag .String ("output" , "" , "output to file instead of STDOUT" )
413+ output := flag .String ("output" , "" , "output to file (can have .gz or .zst ext for compression) instead of STDOUT" )
386414 noProgress := flag .Bool ("no-progress" , false , "disable progress printing" )
387415 progressJSON := flag .String ("progress-json" , "" , "write current progress to a file" )
388416 ver := flag .Bool ("version" , false , "print version and exit" )
@@ -424,19 +452,45 @@ func Main() error { //nolint:funlen,cyclop,gocognit,gocyclo
424452 }
425453
426454 if * output != "" && r .outDir == "" { //nolint:nestif
427- out , err := os .Create (* output )
455+ fn := * output
456+
457+ out , err := os .Create (fn ) //nolint:gosec
428458 if err != nil {
429- return fmt .Errorf ("failed to create output file %s: %w" , * output , err )
459+ return fmt .Errorf ("failed to create output file %s: %w" , fn , err )
430460 }
431461
432- w := bufio .NewWriterSize (out , 64 * 1024 )
462+ r .output = out
463+ compCloser := io .Closer (io .NopCloser (nil ))
464+
465+ switch {
466+ case strings .HasSuffix (fn , ".gz" ):
467+ gw := gzip .NewWriter (r .output )
468+ compCloser = gw
469+
470+ r .output = gw
471+ case strings .HasSuffix (fn , ".zst" ):
472+ zw , err := zstdWriter (r .output )
473+ if err != nil {
474+ return fmt .Errorf ("zstd new writer: %w" , err )
475+ }
476+
477+ compCloser = zw
478+
479+ r .output = zw
480+ }
481+
482+ w := bufio .NewWriterSize (r .output , 64 * 1024 )
433483 r .output = w
434484
435485 defer func () {
436486 if err := w .Flush (); err != nil {
437487 log .Fatalf ("failed to flush STDOUT buffer: %s" , err )
438488 }
439489
490+ if err := compCloser .Close (); err != nil {
491+ log .Fatalf ("failed to close compressor: %s" , err )
492+ }
493+
440494 if err := out .Close (); err != nil {
441495 log .Fatalf ("failed to close output file %s: %s" , * output , err )
442496 }
0 commit comments