textutil: Add PrefixLineWriter
PrefixLineWriter returns a io.Writer wrapper that adds a prefix
to each EOL it encounters. It buffers, and Flush must be called.
Also added the WriteFlusher and WriteFlushCloser interfaces.
Change-Id: I6a3335c682b505f386c890c50e89d39ba174e526
diff --git a/textutil/.api b/textutil/.api
index 278023d..fbf83f1 100644
--- a/textutil/.api
+++ b/textutil/.api
@@ -5,6 +5,7 @@
pkg textutil, func FlushRuneChunk(RuneChunkDecoder, func(rune) error) error
pkg textutil, func NewLineWriter(io.Writer, int, RuneChunkDecoder, RuneEncoder) *LineWriter
pkg textutil, func NewUTF8LineWriter(io.Writer, int) *LineWriter
+pkg textutil, func PrefixLineWriter(io.Writer, string) WriteFlushCloser
pkg textutil, func PrefixWriter(io.Writer, string) io.Writer
pkg textutil, func TerminalSize() (int, int, error)
pkg textutil, func WriteRuneChunk(RuneChunkDecoder, func(rune) error, []byte) (int, error)
@@ -26,3 +27,10 @@
pkg textutil, type RuneEncoder interface, Encode(rune, *bytes.Buffer)
pkg textutil, type UTF8ChunkDecoder struct
pkg textutil, type UTF8Encoder struct
+pkg textutil, type WriteFlushCloser interface { Close, Flush, Write }
+pkg textutil, type WriteFlushCloser interface, Close() error
+pkg textutil, type WriteFlushCloser interface, Flush() error
+pkg textutil, type WriteFlushCloser interface, Write([]byte) (int, error)
+pkg textutil, type WriteFlusher interface { Flush, Write }
+pkg textutil, type WriteFlusher interface, Flush() error
+pkg textutil, type WriteFlusher interface, Write([]byte) (int, error)
diff --git a/textutil/writer.go b/textutil/writer.go
index 9bbec00..8b7e930 100644
--- a/textutil/writer.go
+++ b/textutil/writer.go
@@ -7,8 +7,29 @@
import (
"bytes"
"io"
+ "unicode/utf8"
)
+// WriteFlusher is the interface that groups the basic Write and Flush methods.
+//
+// Flush is typically provided when Write calls perform buffering; Flush
+// immediately outputs the buffered data. Flush must be called after the last
+// call to Write, and may be called an arbitrary number of times before the last
+// Write.
+//
+// If the type is also a Closer, Close implies a Flush call.
+type WriteFlusher interface {
+ io.Writer
+ Flush() error
+}
+
+// WriteFlushCloser is the interface that groups the basic Write, Flush and
+// Close methods.
+type WriteFlushCloser interface {
+ WriteFlusher
+ io.Closer
+}
+
// PrefixWriter returns an io.Writer that wraps w, where the prefix is written
// out immediately before the first non-empty Write call.
func PrefixWriter(w io.Writer, prefix string) io.Writer {
@@ -28,6 +49,77 @@
return w.w.Write(data)
}
+// PrefixLineWriter returns a WriteFlushCloser that wraps w. Any occurrence of
+// EOL (\f, \n, \r, \v, LineSeparator or ParagraphSeparator) causes the
+// preceeding line to be written to w, with the given prefix. Data without EOL
+// is buffered until the next EOL, or Flush or Close call. A single Write call
+// may result in zero or more Write calls on the underlying writer.
+func PrefixLineWriter(w io.Writer, prefix string) WriteFlushCloser {
+ return &prefixLineWriter{w, []byte(prefix), nil}
+}
+
+type prefixLineWriter struct {
+ w io.Writer
+ prefix []byte
+ buf []byte
+}
+
+const eolRunesAsString = "\f\n\r\v" + string(LineSeparator) + string(ParagraphSeparator)
+
+func (w *prefixLineWriter) Write(data []byte) (int, error) {
+ totalLen := len(data)
+ for len(data) > 0 {
+ index := bytes.IndexAny(data, eolRunesAsString)
+ if index == -1 {
+ // No EOL: buffer remaining data.
+ // TODO(toddw): Flush at a max size, to avoid unbounded growth?
+ w.buf = append(w.buf, data...)
+ return totalLen, nil
+ }
+ // Saw EOL: write prefix, buffer, and data including EOL.
+ if _, err := w.w.Write(w.prefix); err != nil {
+ return totalLen - len(data), err
+ }
+ if _, err := w.w.Write(w.buf); err != nil {
+ return totalLen - len(data), err
+ }
+ w.buf = w.buf[:0]
+ _, eolSize := utf8.DecodeRune(data[index:])
+ n, err := w.w.Write(data[:index+eolSize])
+ data = data[n:]
+ if err != nil {
+ return totalLen - len(data), err
+ }
+ }
+ return totalLen, nil
+}
+
+func (w *prefixLineWriter) Flush() error {
+ if len(w.buf) > 0 {
+ if _, err := w.w.Write(w.prefix); err != nil {
+ return err
+ }
+ if _, err := w.w.Write(w.buf); err != nil {
+ return err
+ }
+ w.buf = w.buf[:0]
+ }
+ if f, ok := w.w.(WriteFlusher); ok {
+ return f.Flush()
+ }
+ return nil
+}
+
+func (w *prefixLineWriter) Close() error {
+ firstErr := w.Flush()
+ if c, ok := w.w.(io.Closer); ok {
+ if err := c.Close(); firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
+
// ByteReplaceWriter returns an io.Writer that wraps w, where all occurrences of
// the old byte are replaced with the new string on Write calls.
func ByteReplaceWriter(w io.Writer, old byte, new string) io.Writer {
diff --git a/textutil/writer_test.go b/textutil/writer_test.go
index 3db70e9..cb8f705 100644
--- a/textutil/writer_test.go
+++ b/textutil/writer_test.go
@@ -7,6 +7,7 @@
import (
"bytes"
"fmt"
+ "strings"
"testing"
)
@@ -22,18 +23,39 @@
{"", []string{"a", ""}, "a"},
{"", []string{"", "a"}, "a"},
{"", []string{"a", "b"}, "ab"},
+ {"", []string{"ab"}, "ab"},
+ {"", []string{"\n"}, "\n"},
+ {"", []string{"\n", ""}, "\n"},
+ {"", []string{"", "\n"}, "\n"},
+ {"", []string{"a", "\n"}, "a\n"},
+ {"", []string{"a\n"}, "a\n"},
+ {"", []string{"\n", "a"}, "\na"},
+ {"", []string{"\na"}, "\na"},
+ {"", []string{"a\nb\nc"}, "a\nb\nc"},
{"PRE", nil, ""},
{"PRE", []string{""}, ""},
{"PRE", []string{"a"}, "PREa"},
{"PRE", []string{"a", ""}, "PREa"},
{"PRE", []string{"", "a"}, "PREa"},
{"PRE", []string{"a", "b"}, "PREab"},
+ {"PRE", []string{"ab"}, "PREab"},
+ {"PRE", []string{"\n"}, "PRE\n"},
+ {"PRE", []string{"\n", ""}, "PRE\n"},
+ {"PRE", []string{"", "\n"}, "PRE\n"},
+ {"PRE", []string{"a", "\n"}, "PREa\n"},
+ {"PRE", []string{"a\n"}, "PREa\n"},
+ {"PRE", []string{"\n", "a"}, "PRE\na"},
+ {"PRE", []string{"\na"}, "PRE\na"},
+ {"PRE", []string{"a", "\n", "b", "\n", "c"}, "PREa\nb\nc"},
+ {"PRE", []string{"a\nb\nc"}, "PREa\nb\nc"},
+ {"PRE", []string{"a\nb\nc\n"}, "PREa\nb\nc\n"},
}
for _, test := range tests {
var buf bytes.Buffer
w := PrefixWriter(&buf, test.Prefix)
+ name := fmt.Sprintf("(%q, %q)", test.Want, test.Writes)
for _, write := range test.Writes {
- name := fmt.Sprintf("(%v, %v)", test.Want, write)
+ name := name + fmt.Sprintf("(%q)", write)
n, err := w.Write([]byte(write))
if got, want := n, len(write); got != want {
t.Errorf("%s got len %d, want %d", name, got, want)
@@ -43,11 +65,99 @@
}
}
if got, want := buf.String(), test.Want; got != want {
- t.Errorf("got %v, want %v", got, want)
+ t.Errorf("%s got %q, want %q", name, got, want)
}
}
}
+func TestPrefixLineWriter(t *testing.T) {
+ tests := []struct {
+ Prefix string
+ Writes []string
+ Want string
+ }{
+ {"", nil, ""},
+ {"", []string{""}, ""},
+ {"", []string{"a"}, "a"},
+ {"", []string{"a", ""}, "a"},
+ {"", []string{"", "a"}, "a"},
+ {"", []string{"a", "b"}, "ab"},
+ {"", []string{"ab"}, "ab"},
+ {"", []string{"\n"}, "\n"},
+ {"", []string{"\n", ""}, "\n"},
+ {"", []string{"", "\n"}, "\n"},
+ {"", []string{"a", "\n"}, "a\n"},
+ {"", []string{"a\n"}, "a\n"},
+ {"", []string{"\n", "a"}, "\na"},
+ {"", []string{"\na"}, "\na"},
+ {"", []string{"a\nb\nc"}, "a\nb\nc"},
+ {"PRE", nil, ""},
+ {"PRE", []string{""}, ""},
+ {"PRE", []string{"a"}, "PREa"},
+ {"PRE", []string{"a", ""}, "PREa"},
+ {"PRE", []string{"", "a"}, "PREa"},
+ {"PRE", []string{"a", "b"}, "PREab"},
+ {"PRE", []string{"ab"}, "PREab"},
+ {"PRE", []string{"\n"}, "PRE\n"},
+ {"PRE", []string{"\n", ""}, "PRE\n"},
+ {"PRE", []string{"", "\n"}, "PRE\n"},
+ {"PRE", []string{"a", "\n"}, "PREa\n"},
+ {"PRE", []string{"a\n"}, "PREa\n"},
+ {"PRE", []string{"\n", "a"}, "PRE\nPREa"},
+ {"PRE", []string{"\na"}, "PRE\nPREa"},
+ {"PRE", []string{"a", "\n", "b", "\n", "c"}, "PREa\nPREb\nPREc"},
+ {"PRE", []string{"a\nb\nc"}, "PREa\nPREb\nPREc"},
+ {"PRE", []string{"a\nb\nc\n"}, "PREa\nPREb\nPREc\n"},
+ }
+ for _, test := range tests {
+ for _, eol := range eolRunesAsString {
+ // Replace \n in Want and Writes with the test eol rune.
+ want := strings.Replace(test.Want, "\n", string(eol), -1)
+ var writes []string
+ for _, write := range test.Writes {
+ writes = append(writes, strings.Replace(write, "\n", string(eol), -1))
+ }
+ // Run the actual tests.
+ var buf bytes.Buffer
+ w := PrefixLineWriter(&buf, test.Prefix)
+ name := fmt.Sprintf("(%q, %q)", want, writes)
+ for _, write := range writes {
+ name := name + fmt.Sprintf("(%q)", write)
+ n, err := w.Write([]byte(write))
+ if got, want := n, len(write); got != want {
+ t.Errorf("%s got len %d, want %d", name, got, want)
+ }
+ if err != nil {
+ t.Errorf("%s got error: %v", name, err)
+ }
+ }
+ if err := w.Flush(); err != nil {
+ t.Errorf("%s Flush got error: %v", name, err)
+ }
+ if got, want := buf.String(), want; got != want {
+ t.Errorf("%s got %q, want %q", name, got, want)
+ }
+ }
+ }
+}
+
+type fakeWriteFlushCloser struct{ flushed, closed bool }
+
+func (f *fakeWriteFlushCloser) Write(p []byte) (int, error) { return len(p), nil }
+func (f *fakeWriteFlushCloser) Flush() error { f.flushed = true; return nil }
+func (f *fakeWriteFlushCloser) Close() error { f.closed = true; return nil }
+
+func TestPrefixLineWriterCloseFlush(t *testing.T) {
+ var fake fakeWriteFlushCloser
+ w := PrefixLineWriter(&fake, "")
+ if w.Flush(); !fake.flushed {
+ t.Errorf("Flush not propagated")
+ }
+ if w.Close(); !fake.closed {
+ t.Errorf("Close not propagated")
+ }
+}
+
func TestByteReplaceWriter(t *testing.T) {
tests := []struct {
Old byte
@@ -77,8 +187,9 @@
for _, test := range tests {
var buf bytes.Buffer
w := ByteReplaceWriter(&buf, test.Old, test.New)
+ name := fmt.Sprintf("(%q, %q, %q, %q)", test.Old, test.New, test.Want, test.Writes)
for _, write := range test.Writes {
- name := fmt.Sprintf("(%v, %v, %v, %v)", test.Old, test.New, test.Want, write)
+ name := name + fmt.Sprintf("(%q)", write)
n, err := w.Write([]byte(write))
if got, want := n, len(write); got != want {
t.Errorf("%s got len %d, want %d", name, got, want)
@@ -88,7 +199,7 @@
}
}
if got, want := buf.String(), test.Want; got != want {
- t.Errorf("got %v, want %v", got, want)
+ t.Errorf("%s got %q, want %q", name, got, want)
}
}
}