diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-08-27 16:07:05 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-08-27 16:07:05 +0900 |
| commit | ccb004e2174ca9fedba7bc95b036fc62dbdc28b1 (patch) | |
| tree | d8b0603060d3cec0ee28f4cd55ac88c2560c1ce0 /internal | |
| parent | ebb1144efc6c09dff8a732c5baf11db5b8f515b2 (diff) | |
internal/pkg: treat bare carriage return as end-of-line marker
This avoids having progress indicators mess up logging.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/pkg/exec.go | 32 | ||||
| -rw-r--r-- | internal/pkg/exec_test.go | 93 |
2 files changed, 124 insertions, 1 deletions
diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go index 7c4133eb..e499ecf1 100644 --- a/internal/pkg/exec.go +++ b/internal/pkg/exec.go @@ -2,6 +2,7 @@ package pkg import ( "bufio" + "bytes" "context" "errors" "fmt" @@ -439,6 +440,34 @@ const ( execWaitDelay = time.Nanosecond ) +// scanLinesCR is like [bufio.ScanLines], but also treats a bare \r as an +// end-of-line marker. +func scanLinesCR(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + ri, ni := bytes.IndexByte(data, '\r'), bytes.IndexByte(data, '\n') + + if ri >= 0 && (ni < 0 || ri < ni) { + if ri+1 == ni { + // We have a full \r\n-terminated line. + return ri + 2, data[:ri], nil + } + // We have a bare \r, probably some kind of progress indicator. + return ri + 1, data[:ri], nil + } + if ni >= 0 && (ri < 0 || ni < ri) { + // We have a full newline-terminated line. + return ni + 1, data[:ni], nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} + // scanVerbose prefixes program output for a verbose [message.Msg]. func scanVerbose( msg message.Msg, @@ -449,6 +478,7 @@ func scanVerbose( ) { defer close(done) s := bufio.NewScanner(r) + s.Split(scanLinesCR) s.Buffer( make([]byte, bufio.MaxScanTokenSize), bufio.MaxScanTokenSize<<12, @@ -458,7 +488,7 @@ func scanVerbose( } if err := s.Err(); err != nil && !errors.Is(err, os.ErrClosed) { cancel() - msg.Verbose("*"+prefix, err) + msg.Verbose("*"+prefix, err.Error()+suffix) } } diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go index 222ab790..408d6257 100644 --- a/internal/pkg/exec_test.go +++ b/internal/pkg/exec_test.go @@ -1,6 +1,7 @@ package pkg_test import ( + "bufio" "bytes" _ "embed" "encoding/gob" @@ -11,7 +12,9 @@ import ( "os/exec" "path/filepath" "slices" + "strings" "testing" + _ "unsafe" // for go:linkname "hakurei.app/check" "hakurei.app/container" @@ -41,6 +44,96 @@ func init() { }) } +// scanLinesCR is like [bufio.ScanLines], but also treats a bare \r as an +// end-of-line marker. +// +//go:linkname scanLinesCR hakurei.app/internal/pkg.scanLinesCR +func scanLinesCR(data []byte, atEOF bool) (advance int, token []byte, err error) + +func TestScan(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + want []string + }{ + {"progress indicator", "Updating files: 76% (8655/11256)\r" + + "Updating files: 77% (8668/11256)\r" + + "Updating files: 78% (8780/11256)\r" + + "Updating files: 79% (8893/11256)\r" + + "Updating files: 80% (9005/11256)\r" + + "Updating files: 81% (9118/11256)\r" + + "Updating files: 82% (9230/11256)\r" + + "Updating files: 83% (9343/11256)\r" + + "Updating files: 84% (9456/11256)\r" + + "Updating files: 85% (9568/11256)\r" + + "Updating files: 86% (9681/11256)\r" + + "Updating files: 87% (9793/11256)\r" + + "Updating files: 88% (9906/11256)\r" + + "Updating files: 89% (10018/11256)\r" + + "Updating files: 90% (10131/11256)\r" + + "Updating files: 91% (10243/11256)\r" + + "Updating files: 92% (10356/11256)\r" + + "Updating files: 93% (10469/11256)\r" + + "Updating files: 94% (10581/11256)\r" + + "Updating files: 95% (10694/11256)\r" + + "Updating files: 96% (10806/11256)\r" + + "Updating files: 97% (10919/11256)\r" + + "Updating files: 98% (11031/11256)\r" + + "Updating files: 99% (11144/11256)\r" + + "Updating files: 100% (11256/11256)\r" + + "Updating files: 100% (11256/11256), done.\n", []string{ + "Updating files: 76% (8655/11256)", + "Updating files: 77% (8668/11256)", + "Updating files: 78% (8780/11256)", + "Updating files: 79% (8893/11256)", + "Updating files: 80% (9005/11256)", + "Updating files: 81% (9118/11256)", + "Updating files: 82% (9230/11256)", + "Updating files: 83% (9343/11256)", + "Updating files: 84% (9456/11256)", + "Updating files: 85% (9568/11256)", + "Updating files: 86% (9681/11256)", + "Updating files: 87% (9793/11256)", + "Updating files: 88% (9906/11256)", + "Updating files: 89% (10018/11256)", + "Updating files: 90% (10131/11256)", + "Updating files: 91% (10243/11256)", + "Updating files: 92% (10356/11256)", + "Updating files: 93% (10469/11256)", + "Updating files: 94% (10581/11256)", + "Updating files: 95% (10694/11256)", + "Updating files: 96% (10806/11256)", + "Updating files: 97% (10919/11256)", + "Updating files: 98% (11031/11256)", + "Updating files: 99% (11144/11256)", + "Updating files: 100% (11256/11256)", + "Updating files: 100% (11256/11256), done.", + }}, + + {"crlf", "0\r1\n2\n3\r\n4\n5\r6\r\n7\n", []string{ + "0", "1", "2", "3", "4", "5", "6", "7", + }}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := bufio.NewScanner(strings.NewReader(tc.data)) + s.Split(scanLinesCR) + got := make([]string, 0, len(tc.want)) + for s.Scan() { + got = append(got, s.Text()) + } + if err := s.Err(); err != nil { + t.Fatal(err) + } + if !slices.Equal(got, tc.want) { + t.Fatalf("Scan: %q, want %q", got, tc.want) + } + }) + } +} + func TestExec(t *testing.T) { t.Parallel() |
