Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
#
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ test:
samples:
echo ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 | go run ./cmd/uni map -a > samples/uni.txt

lint:
go vet ./...

.PHONY: generate-proto imports install install-protoc hyper fast-test test samples
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ go install github.com/ripta/rt/cmd/...@latest

or pick-and-choose each tool to individually install:

* [cg](#cg) to run a command and annotate its output with timestamps
* [enc](#enc) to encode and decode STDIN
* [grpcto](#grpcto) to frame and unframe gRPC messages
* [hs](#hs) to hash STDIN
Expand All @@ -43,6 +44,72 @@ Pull requests welcome, though you should probably check first before sinking any



`cg`
----

Run a command and annotate each line of its stdout and stderr with a timestamp
and stream indicator (`O` for stdout, `E` for stderr, `I` for cg's own
lifecycle messages).

Acts like the `annotate-output` script; `cg` is short for command guard.

```
go install github.com/ripta/rt/cmd/cg@latest
```

Basic usage:

```
❯ cg -- echo hello
19:02:59 I: cg v0.1.0
19:02:59 I: prefix="15:04:05 "
19:02:59 I: Started echo hello
19:02:59 O: hello
19:02:59 I: Finished with exitcode 0
```

Stdout and stderr are distinguished:

```
❯ cg -- sh -c 'echo out; echo err >&2'
19:03:04 I: cg v0.1.0
19:03:04 I: prefix="15:04:05 "
19:03:04 I: Started sh -c 'echo out; echo err >&2'
19:03:04 O: out
19:03:04 E: err
19:03:04 I: Finished with exitcode 0
```

The child's exit code is propagated:

```
❯ cg -- sh -c 'exit 42'; echo $?
19:20:35 I: cg v0.1.0
19:20:35 I: prefix="15:04:05 "
19:20:35 I: Started sh -c 'exit 42'
19:20:35 I: Finished with exitcode 42
42
```

Use `--format` to change the timestamp prefix. It takes the golang
`time.Format` layout:

```
❯ cg --format '2006-01-02T15:04:05 ' -- echo hello
2026-02-22T19:05:00 I: cg v0.1.0
2026-02-22T19:05:00 I: prefix="2006-01-02T15:04:05 "
2026-02-22T19:05:00 I: Started echo hello
2026-02-22T19:05:00 O: hello
2026-02-22T19:05:00 I: Finished with exitcode 0
```

Signals SIGINT and SIGTERM are forwarded to the child process.

`cg` also supports `--capture` to capture the child's stdout and stderr into
separate files, and `--buffered` to buffer the child's output and print it all
at once when the child finishes, instead of streaming it in real time.


`enc`
----

Expand Down
28 changes: 28 additions & 0 deletions cmd/cg/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"errors"
"fmt"
"os"

"github.com/ripta/rt/pkg/cg"
"github.com/ripta/rt/pkg/version"
)

func main() {
cmd := cg.NewCommand()
cmd.AddCommand(version.NewCommand())

err := cmd.Execute()
if err == nil {
return
}

var exitErr *cg.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.Code)
}

fmt.Fprintf(os.Stderr, "Error: %+v\n", err)
os.Exit(1)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/ripta/hypercmd v0.5.0
github.com/ripta/reals v0.0.0-20251220032726-c99f163d5c5c
github.com/ripta/unihan v0.0.0-20250404091138-c307c698a880
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
Expand Down
20 changes: 3 additions & 17 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
Expand All @@ -69,15 +67,14 @@ github.com/r3labs/diff/v3 v3.0.2 h1:yVuxAY1V6MeM4+HNur92xkS39kB/N+cFi2hMkY06BbA=
github.com/r3labs/diff/v3 v3.0.2/go.mod h1:Cy542hv0BAEmhDYWtGxXRQ4kqRsVIcEjG9gChUlTmkw=
github.com/ripta/hypercmd v0.5.0 h1:8wEZndeP/umK8xLgZD1aYOIsdWsxymweJSETnbF1Awo=
github.com/ripta/hypercmd v0.5.0/go.mod h1:nffU7nnFN8yU/PIHbN35UCE5q0FSnDJ6ev45SFEIZ48=
github.com/ripta/reals v0.0.0-20251129121815-4fa2f223ded2 h1:QWeZ/uw8S951/qJQzg+wBAOpFhUx7yVJxyPRZdjJmuI=
github.com/ripta/reals v0.0.0-20251129121815-4fa2f223ded2/go.mod h1:WErCt40puDDQdpVq8Hg1DzjB0svufA8WboSYG4BI2+E=
github.com/ripta/reals v0.0.0-20251220032726-c99f163d5c5c h1:4bBR+jNoWIs1roinlXrVDUtmSvqjtNbrJ3cuQtFci5g=
github.com/ripta/reals v0.0.0-20251220032726-c99f163d5c5c/go.mod h1:WErCt40puDDQdpVq8Hg1DzjB0svufA8WboSYG4BI2+E=
github.com/ripta/unihan v0.0.0-20250404091138-c307c698a880 h1:ZzDUYlZP/LHJmkh+PtgRZHEKa+eNVefq6YR8BnUCQ2I=
github.com/ripta/unihan v0.0.0-20250404091138-c307c698a880/go.mod h1:ZLBfCas48lym/27GOsyFjRo7OGejoGHzOTdUdoRtDqU=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
Expand All @@ -102,17 +99,12 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -126,16 +118,10 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 h1:jm6v6kMRpTYKxBRrDkYAitNJegUeO1Mf3Kt80obv0gg=
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9/go.mod h1:LmwNphe5Afor5V3R5BppOULHOnt2mCIf+NxMd4XiygE=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
Expand Down
2 changes: 2 additions & 0 deletions hypercmd/rt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/ripta/hypercmd/pkg/hypercmd"

"github.com/ripta/rt/pkg/calc"
"github.com/ripta/rt/pkg/cg"
"github.com/ripta/rt/pkg/enc"
"github.com/ripta/rt/pkg/grpcto"
"github.com/ripta/rt/pkg/hashsum"
Expand Down Expand Up @@ -38,6 +39,7 @@ func main() {
root.AddCommand(streamdiff.NewCommand())

root.AddCommand(calc.NewCommand())
root.AddCommand(cg.NewCommand())

v := version.NewCommand()
root.Root().AddCommand(v)
Expand Down
129 changes: 129 additions & 0 deletions pkg/cg/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cg

import (
"bufio"
"io"
"sync"
)

type bufferedLine struct {
prefix string
line string
partial bool
}

// LineBuffer accumulates child output lines in memory, grouped by indicator.
// Lines are stored with the prefix captured at receive time so they can be
// replayed later with their original timestamps.
type LineBuffer struct {
mu sync.Mutex
prefix PrefixFunc
proc LineProcessor
streams map[Indicator][]bufferedLine
}

// SetProcessor sets the line processor for this buffer.
func (b *LineBuffer) SetProcessor(proc LineProcessor) {
b.proc = proc
}

// NewLineBuffer creates a LineBuffer that calls prefix to obtain the prefix
// string for each line as it arrives.
func NewLineBuffer(prefix PrefixFunc) *LineBuffer {
return &LineBuffer{
prefix: prefix,
streams: make(map[Indicator][]bufferedLine),
}
}

// WriteLines reads line-by-line from r, capturing each line with its
// receive-time prefix under the given indicator. Partial final lines (no
// trailing newline) are recorded with partial set to true.
func (b *LineBuffer) WriteLines(r io.Reader, ind Indicator) error {
br := bufio.NewReader(r)
for {
line, err := br.ReadBytes('\n')
if err != nil {
if err == io.EOF {
if len(line) > 0 {
prefix, display := b.processLine(string(line))
if prefix == "" {
prefix = b.prefix()
}

b.mu.Lock()
b.streams[ind] = append(b.streams[ind], bufferedLine{
prefix: prefix,
line: display,
partial: true,
})
b.mu.Unlock()
}
return nil
}
return err
}

raw := string(line[:len(line)-1])
prefix, display := b.processLine(raw)
if prefix == "" {
prefix = b.prefix()
}

b.mu.Lock()
b.streams[ind] = append(b.streams[ind], bufferedLine{
prefix: prefix,
line: display,
})
b.mu.Unlock()
}
}

func (b *LineBuffer) processLine(line string) (prefix string, display string) {
if b.proc == nil {
return "", line
}

result := b.proc(line)
if result == nil {
return "", line
}

return result.Prefix, result.Line
}

// Flush writes all buffered lines to w, grouped by stream. Stdout lines are
// written first, then stderr. Each non-empty stream is preceded by a section
// header. Lines are replayed with their original receive-time prefix.
func (b *LineBuffer) Flush(w *AnnotatedWriter) error {
b.mu.Lock()
defer b.mu.Unlock()

for _, ind := range []Indicator{IndicatorOut, IndicatorErr} {
lines := b.streams[ind]
if len(lines) == 0 {
continue
}

header := "--- stdout ---"
if ind == IndicatorErr {
header = "--- stderr ---"
}
if err := w.WriteLine(IndicatorInfo, header); err != nil {
return err
}

for _, bl := range lines {
if bl.partial {
if err := w.WritePartialLineWithPrefix(bl.prefix, ind, bl.line); err != nil {
return err
}
} else {
if err := w.WriteLineWithPrefix(bl.prefix, ind, bl.line); err != nil {
return err
}
}
}
}
return nil
}
Loading