term: process bytes returned with a read error The io.Reader contract permits Read to return both data and a non-nil error. readLine currently returns the error before processing the data, which can discard the final complete line when a reader returns data with io.EOF. Append returned bytes to the input buffer and save the accompanying error. Report the error after the buffered input has been processed, preserving it across ReadLine calls when the input contains a complete line. Add a regression test for a reader that returns "line\r" and io.EOF in the same call. Fixes golang/go#80661 Change-Id: I34ac28731ec40f3398b9c0990e19cc17b737ba9e Reviewed-on: https://go-review.googlesource.com/c/term/+/808420 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Alan Donovan <adonovan@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/terminal.go b/terminal.go index 6ec537c..e8327c8 100644 --- a/terminal.go +++ b/terminal.go
@@ -106,6 +106,9 @@ // a read. It aliases into inBuf. remainder []byte inBuf [256]byte + // readErr is an error returned by a read that also returned data. It is + // reported after all of that data has been processed. + readErr error // History records and retrieves lines of input read by [ReadLine] which // a user can retrieve and navigate using the up and down arrow keys. @@ -788,7 +791,11 @@ return } -// ReadLine returns a line of input from the terminal. +// ReadLine returns a line of input from the terminal, excluding the +// trailing newline. It may return partial data along with an error. +// An [io.EOF] error indicates the end of the stream. For other errors, +// such as [ErrPasteIndicator] in bracketed paste mode, a subsequent +// call may return more data. func (t *Terminal) ReadLine() (line string, err error) { t.lock.Lock() defer t.lock.Unlock() @@ -863,21 +870,24 @@ } return } + if t.readErr != nil { + err = t.readErr + t.readErr = nil + return + } // t.remainder is a slice at the beginning of t.inBuf // containing a partial key sequence readBuf := t.inBuf[len(t.remainder):] - var n int t.lock.Unlock() - n, err = t.c.Read(readBuf) + n, readErr := t.c.Read(readBuf) t.lock.Lock() - if err != nil { - return - } - t.remainder = t.inBuf[:n+len(t.remainder)] + if readErr != nil { + t.readErr = readErr + } } }
diff --git a/terminal_test.go b/terminal_test.go index 7d0afd8..7d8d3d5 100644 --- a/terminal_test.go +++ b/terminal_test.go
@@ -56,6 +56,44 @@ } } +func TestReadLineProcessesDataWithEOF(t *testing.T) { + c := &dataAndEOFReader{data: []byte("line\r")} + ss := NewTerminal(c, "> ") + line, err := ss.ReadLine() + if err != nil { + t.Fatalf("ReadLine returned error with line data: %v", err) + } + if line != "line" { + t.Fatalf("ReadLine returned %q, want %q", line, "line") + } + + line, err = ss.ReadLine() + if line != "" || err != io.EOF { + t.Fatalf("ReadLine after buffered data returned (%q, %v), want (%q, EOF)", line, err, "") + } + if c.reads != 1 { + t.Fatalf("ReadLine performed %d reads, want 1", c.reads) + } +} + +type dataAndEOFReader struct { + data []byte + reads int +} + +func (r *dataAndEOFReader) Read(p []byte) (int, error) { + if r.reads > 0 { + return 0, io.EOF + } + r.reads++ + n := copy(p, r.data) + return n, io.EOF +} + +func (r *dataAndEOFReader) Write(p []byte) (int, error) { + return len(p), nil +} + var keyPressTests = []struct { in string line string