├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── A │ ├── 1 │ └── 2 └── message ├── go.mod ├── io.go ├── prefix.go ├── prefix_test.go ├── tree.go └── tree_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /go/src/github.com/segmentio/textio 5 | docker: 6 | - image: circleci/golang 7 | steps: 8 | - checkout 9 | - run: go test -v -race -cover ./... 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Emacs 15 | *~ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Segment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # textio [![CircleCI](https://circleci.com/gh/segmentio/textio.svg?style=shield)](https://circleci.com/gh/segmentio/textio) [![Go Report Card](https://goreportcard.com/badge/github.com/segmentio/textio)](https://goreportcard.com/report/github.com/segmentio/textio) [![GoDoc](https://godoc.org/github.com/segmentio/textio?status.svg)](https://godoc.org/github.com/segmentio/textio) 2 | 3 | > **Note** 4 | > Segment has paused maintenance on this project, but may return it to an active status in the future. Issues and pull requests from external contributors are not being considered, although internal contributions may appear from time to time. The project remains available under its open source license for anyone to use. 5 | 6 | Go package providing tools for advanced text manipulations 7 | 8 | ## Motivation 9 | 10 | This package aims to provide a suite of tools to deal with text parsing and 11 | formatting. It is intended to extend what the standard library already offers, 12 | and make it easy to integrate with it. 13 | 14 | ## Examples 15 | 16 | This sections presents a couple of examples about how to use this package. 17 | 18 | ### Indenting 19 | 20 | Indentation is often a complex problem to solve when dealing with stream of text 21 | that may be composed of multiple lines. To address this problem, this package 22 | provides the `textio.PrefixWriter` type, which implements the `io.Writer` 23 | interface and automatically prepends every line of output with a predefined 24 | prefix. 25 | 26 | Here is an example: 27 | ```go 28 | func copyIndent(w io.Writer, r io.Reader) error { 29 | p := textio.NewPrefixWriter(w, "\t") 30 | 31 | // Copy data from an input stream into the PrefixWriter, all lines will 32 | // be prefixed with a '\t' character. 33 | if _, err := io.Copy(p, r); err != nil { 34 | return err 35 | } 36 | 37 | // Flushes any data buffered in the PrefixWriter, this is important in 38 | // case the last line was not terminated by a '\n' character. 39 | return p.Flush() 40 | } 41 | ``` 42 | 43 | ### Tree Formatting 44 | 45 | A common way to represent tree-like structures is the formatting used by the 46 | `tree(1)` unix command. The `textio.TreeWriter` type is an implementation of 47 | an `io.Writer` which supports this kind of output. It works in a recursive 48 | fashion where nodes created from a parent tree writer are formatted as part 49 | of that tree structure. 50 | 51 | Here is an example: 52 | ```go 53 | func ls(w io.Writer, path string) { 54 | tree := NewTreeWriter(w) 55 | tree.WriteString(filepath.Base(path)) 56 | defer tree.Close() 57 | 58 | files, _ := ioutil.ReadDir(path) 59 | 60 | for _, f := range files { 61 | if f.Mode().IsDir() { 62 | ls(tree, filepath.Join(path, f.Name())) 63 | } 64 | } 65 | 66 | for _, f := range files { 67 | if !f.Mode().IsDir() { 68 | io.WriteString(NewTreeWriter(tree), f.Name()) 69 | } 70 | } 71 | } 72 | 73 | ... 74 | 75 | ls(os.Stdout, "examples") 76 | ``` 77 | Which gives this output: 78 | ``` 79 | examples 80 | ├── A 81 | │ ├── 1 82 | │ └── 2 83 | └── message 84 | ``` 85 | -------------------------------------------------------------------------------- /examples/A/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/textio/422277ce0e29f5b7f997fda30ded6140832284e9/examples/A/1 -------------------------------------------------------------------------------- /examples/A/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/textio/422277ce0e29f5b7f997fda30ded6140832284e9/examples/A/2 -------------------------------------------------------------------------------- /examples/message: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/segmentio/textio 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package textio 2 | 3 | import "io" 4 | 5 | // Base returns the direct base of w, which may be w itself if it had no base 6 | // writer. 7 | func Base(w io.Writer) io.Writer { 8 | if d, ok := w.(decorator); ok { 9 | return coalesceWriters(d.Base(), w) 10 | } 11 | return w 12 | } 13 | 14 | // Root returns the root writer of w, which is found by going up the list of 15 | // base writers. 16 | // 17 | // The node is usually the writer where the content ends up being written. 18 | func Root(w io.Writer) io.Writer { 19 | switch x := w.(type) { 20 | case tree: 21 | return coalesceWriters(x.Root(), w) 22 | case node: 23 | return coalesceWriters(Root(x.Parent()), w) 24 | case decorator: 25 | return coalesceWriters(Root(x.Base()), w) 26 | default: 27 | return w 28 | } 29 | } 30 | 31 | // Parent returns the parent writer of w, which is usually a writer of a similar 32 | // type on tree-like writer structures. 33 | func Parent(w io.Writer) io.Writer { 34 | switch x := w.(type) { 35 | case node: 36 | return coalesceWriters(x.Parent(), w) 37 | case decorator: 38 | return coalesceWriters(Parent(x.Base()), w) 39 | default: 40 | return x 41 | } 42 | } 43 | 44 | type decorator interface { 45 | Base() io.Writer 46 | } 47 | 48 | type node interface { 49 | Parent() io.Writer 50 | } 51 | 52 | type tree interface { 53 | Root() io.Writer 54 | } 55 | 56 | func coalesceWriters(writers ...io.Writer) io.Writer { 57 | for _, w := range writers { 58 | if w != nil { 59 | return w 60 | } 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /prefix.go: -------------------------------------------------------------------------------- 1 | package textio 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // PrefixWriter is an implementation of io.Writer which places a prefix before 10 | // every line. 11 | // 12 | // Instances of PrefixWriter are not safe to use concurrently from multiple 13 | // goroutines. 14 | type PrefixWriter struct { 15 | writer io.Writer 16 | indent []byte 17 | buffer []byte 18 | offset int 19 | } 20 | 21 | // NewPrefixWriter constructs a PrefixWriter which outputs to w and prefixes 22 | // every line with s. 23 | func NewPrefixWriter(w io.Writer, s string) *PrefixWriter { 24 | return &PrefixWriter{ 25 | writer: w, 26 | indent: copyStringToBytes(s), 27 | buffer: make([]byte, 0, 256), 28 | } 29 | } 30 | 31 | // Base returns the underlying writer that w outputs to. 32 | func (w *PrefixWriter) Base() io.Writer { 33 | return w.writer 34 | } 35 | 36 | // Buffered returns a byte slice of the data currently buffered in the writer. 37 | func (w *PrefixWriter) Buffered() []byte { 38 | return w.buffer[w.offset:] 39 | } 40 | 41 | // Write writes b to w, satisfies the io.Writer interface. 42 | func (w *PrefixWriter) Write(b []byte) (int, error) { 43 | var c int 44 | var n int 45 | var err error 46 | 47 | forEachLine(b, func(line []byte) bool { 48 | // Always buffer so the input slice doesn't escape and WriteString won't 49 | // copy the string (it saves a dynamic memory allocation on every call 50 | // to WriteString). 51 | w.buffer = append(w.buffer, line...) 52 | 53 | if chunk := w.Buffered(); isLine(chunk) { 54 | c, err = w.writeLine(chunk) 55 | w.discard(c) 56 | } 57 | 58 | n += len(line) 59 | return err == nil 60 | }) 61 | 62 | return n, err 63 | } 64 | 65 | // WriteString writes s to w. 66 | func (w *PrefixWriter) WriteString(s string) (int, error) { 67 | return w.Write([]byte(s)) 68 | } 69 | 70 | // Flush forces all buffered data to be flushed to the underlying writer. 71 | func (w *PrefixWriter) Flush() error { 72 | n, err := w.write(w.buffer) 73 | w.discard(n) 74 | return err 75 | } 76 | 77 | // Width satisfies the fmt.State interface. 78 | func (w *PrefixWriter) Width() (int, bool) { 79 | f, ok := Base(w).(fmt.State) 80 | if ok { 81 | return f.Width() 82 | } 83 | return 0, false 84 | } 85 | 86 | // Precision satisfies the fmt.State interface. 87 | func (w *PrefixWriter) Precision() (int, bool) { 88 | f, ok := Base(w).(fmt.State) 89 | if ok { 90 | return f.Precision() 91 | } 92 | return 0, false 93 | } 94 | 95 | // Flag satisfies the fmt.State interface. 96 | func (w *PrefixWriter) Flag(c int) bool { 97 | f, ok := Base(w).(fmt.State) 98 | if ok { 99 | return f.Flag(c) 100 | } 101 | return false 102 | } 103 | 104 | func (w *PrefixWriter) writeLine(b []byte) (int, error) { 105 | if _, err := w.write(w.indent); err != nil { 106 | return 0, err 107 | } 108 | return w.write(b) 109 | } 110 | 111 | func (w *PrefixWriter) write(b []byte) (int, error) { 112 | return w.writer.Write(b) 113 | } 114 | 115 | func (w *PrefixWriter) discard(n int) { 116 | if n > 0 { 117 | w.offset += n 118 | 119 | switch { 120 | case w.offset == len(w.buffer): 121 | w.buffer = w.buffer[:0] 122 | w.offset = 0 123 | 124 | case w.offset > (cap(w.buffer) / 2): 125 | copy(w.buffer, w.buffer[w.offset:]) 126 | w.buffer = w.buffer[:len(w.buffer)-w.offset] 127 | w.offset = 0 128 | } 129 | } 130 | } 131 | 132 | func copyStringToBytes(s string) []byte { 133 | b := make([]byte, len(s)) 134 | copy(b, s) 135 | return b 136 | } 137 | 138 | func forEachLine(b []byte, do func([]byte) bool) { 139 | for len(b) != 0 { 140 | i := bytes.IndexByte(b, '\n') 141 | 142 | if i < 0 { 143 | i = len(b) 144 | } else { 145 | i++ // include the newline character 146 | } 147 | 148 | if !do(b[:i]) { 149 | break 150 | } 151 | 152 | b = b[i:] 153 | } 154 | } 155 | 156 | func isLine(b []byte) bool { 157 | return len(b) != 0 && b[len(b)-1] == '\n' 158 | } 159 | 160 | var ( 161 | _ fmt.State = (*PrefixWriter)(nil) 162 | ) 163 | -------------------------------------------------------------------------------- /prefix_test.go: -------------------------------------------------------------------------------- 1 | package textio 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestPrefixWriter(t *testing.T) { 10 | b := &bytes.Buffer{} 11 | b.WriteByte('\n') 12 | 13 | w1 := NewPrefixWriter(b, "\t") 14 | w2 := NewPrefixWriter(w1, "\t- ") 15 | 16 | fmt.Fprint(w1, "hello:\n") 17 | 18 | fmt.Fprint(w2, "value: 1") 19 | fmt.Fprint(w2, "\n") 20 | fmt.Fprint(w2, "value: 2\nvalue: 3\n") 21 | 22 | w2.Flush() 23 | w1.Flush() 24 | 25 | const expected = ` 26 | hello: 27 | - value: 1 28 | - value: 2 29 | - value: 3 30 | ` 31 | 32 | found := b.String() 33 | 34 | if expected != found { 35 | t.Error("content mismatch") 36 | t.Log("expected:", expected) 37 | t.Log("found:", found) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package textio 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "unicode/utf8" 8 | ) 9 | 10 | // TreeWriter is an implementation of an io.Writer which prints a tree-like 11 | // representation of the content. 12 | // 13 | // Instances of TreeWriter are not safe to use concurrently from multiple 14 | // goroutines. 15 | type TreeWriter struct { 16 | writer io.Writer 17 | children []*TreeWriter 18 | content []byte 19 | } 20 | 21 | // NewTreeWriter constructs a new TreeWriter which outputs to w. If w is an 22 | // instance of TreeWriter itself the new writer is added to the list of child 23 | // nodes that will be renderend. 24 | func NewTreeWriter(w io.Writer) *TreeWriter { 25 | node := &TreeWriter{ 26 | writer: w, 27 | content: make([]byte, 0, 64), 28 | } 29 | 30 | if parent, _ := node.Parent().(*TreeWriter); parent != nil { 31 | if parent.children == nil { 32 | parent.children = make([]*TreeWriter, 0, 8) 33 | } 34 | parent.children = append(parent.children, node) 35 | } 36 | 37 | return node 38 | } 39 | 40 | // Root returns the root of w, which is the node on which calling Close will 41 | // cause the tree to be rendered to the underlying writer. 42 | func (w *TreeWriter) Root() io.Writer { 43 | if p, _ := w.Parent().(*TreeWriter); p != nil { 44 | return p.Root() 45 | } 46 | return w 47 | } 48 | 49 | // Parent returns the parent node of w, which its most direct base of type 50 | // *TreeWriter. 51 | func (w *TreeWriter) Parent() io.Writer { 52 | if p, _ := w.writer.(*TreeWriter); p != nil { 53 | return p 54 | } 55 | return Parent(w.writer) 56 | } 57 | 58 | // Base returns the base writer of w. 59 | func (w *TreeWriter) Base() io.Writer { 60 | return w.writer 61 | } 62 | 63 | // Write writes b to w, satisfies the io.Writer interface. 64 | func (w *TreeWriter) Write(b []byte) (int, error) { 65 | if w.writer == nil { 66 | return 0, io.ErrClosedPipe 67 | } 68 | w.content = append(w.content, b...) 69 | return len(b), nil 70 | } 71 | 72 | // WriteString writes s to w. 73 | func (w *TreeWriter) WriteString(s string) (int, error) { 74 | return w.Write([]byte(s)) 75 | } 76 | 77 | // WriteByte writes b to w. 78 | func (w *TreeWriter) WriteByte(b byte) error { 79 | w.content = append(w.content, b) 80 | return nil 81 | } 82 | 83 | // WriteRune writes r to w. 84 | func (w *TreeWriter) WriteRune(r rune) (int, error) { 85 | b := [8]byte{} 86 | n := utf8.EncodeRune(b[:], r) 87 | w.content = append(w.content, b[:n]...) 88 | return n, nil 89 | } 90 | 91 | // Width satisfies the fmt.State interface. 92 | func (w *TreeWriter) Width() (int, bool) { 93 | f, ok := Base(w.Root()).(fmt.State) 94 | if ok { 95 | return f.Width() 96 | } 97 | return 0, false 98 | } 99 | 100 | // Precision satisfies the fmt.State interface. 101 | func (w *TreeWriter) Precision() (int, bool) { 102 | f, ok := Base(w.Root()).(fmt.State) 103 | if ok { 104 | return f.Precision() 105 | } 106 | return 0, false 107 | } 108 | 109 | // Flag satisfies the fmt.State interface. 110 | func (w *TreeWriter) Flag(c int) bool { 111 | f, ok := Base(w.Root()).(fmt.State) 112 | if ok { 113 | return f.Flag(c) 114 | } 115 | return false 116 | } 117 | 118 | // Close closes w, causing all buffered content to be flushed to its underlying 119 | // writer, and future write operations to error with io.ErrClosedPipe. 120 | func (w *TreeWriter) Close() (err error) { 121 | defer func() { 122 | w.writer = nil 123 | switch x := recover().(type) { 124 | case nil: 125 | case error: 126 | err = x 127 | default: 128 | err = fmt.Errorf("%+v", x) 129 | } 130 | }() 131 | 132 | // Technically we could have each child node write its own representation 133 | // to a buffer, then render a tree from those content buffers. However this 134 | // would require a lot more copying because each tree level would be written 135 | // into the buffer of its parent. 136 | // 137 | // Instead the approach we take here only requires 1 level of buffering, no 138 | // matter how complex the tree is. First the data is buffered into each node 139 | // and when the tree is closed the code walks through each node and write 140 | // their content and the leading tree symbols to the underlying writer. 141 | 142 | for _, c := range w.children { 143 | if err = c.Close(); err != nil { 144 | return err 145 | } 146 | } 147 | 148 | switch w.writer.(type) { 149 | case nil: 150 | // Already closed 151 | case *TreeWriter: 152 | // Sub-node, don't write anything 153 | default: 154 | buffer := [10]string{} 155 | writer := treeWriter{writer: w.writer, symbols: buffer[:0]} 156 | writer.writeTree(treeCtx{length: 1}, w) 157 | } 158 | 159 | return 160 | } 161 | 162 | var ( 163 | _ io.Writer = (*TreeWriter)(nil) 164 | _ io.StringWriter = (*TreeWriter)(nil) 165 | _ fmt.State = (*TreeWriter)(nil) 166 | ) 167 | 168 | type treeCtx struct { 169 | index int // index of the node 170 | length int // number of nodes 171 | needNewLine bool // whether a new line must be printed 172 | } 173 | 174 | func (ctx *treeCtx) last() bool { 175 | return ctx.index == (ctx.length - 1) 176 | } 177 | 178 | type treeWriter struct { 179 | writer io.Writer 180 | symbols []string 181 | } 182 | 183 | func (w *treeWriter) push(ctx treeCtx) { 184 | w.nextLine(ctx) 185 | w.symbols = append(w.symbols, "") 186 | } 187 | 188 | func (w *treeWriter) pop() { 189 | w.symbols = w.symbols[:w.lastIndex()] 190 | } 191 | 192 | func (w *treeWriter) nextNode(ctx treeCtx) { 193 | if ctx.last() { 194 | w.set("└── ") 195 | } else { 196 | w.set("├── ") 197 | } 198 | } 199 | 200 | func (w *treeWriter) nextLine(ctx treeCtx) { 201 | if ctx.last() { 202 | w.set(" ") 203 | } else { 204 | w.set("│ ") 205 | } 206 | } 207 | 208 | func (w *treeWriter) lastIndex() int { 209 | return len(w.symbols) - 1 210 | } 211 | 212 | func (w *treeWriter) empty() bool { 213 | return len(w.symbols) == 0 214 | } 215 | 216 | func (w *treeWriter) set(s string) { 217 | if !w.empty() { 218 | w.symbols[w.lastIndex()] = s 219 | } 220 | } 221 | 222 | func (w *treeWriter) writeTree(ctx treeCtx, node *TreeWriter) { 223 | w.writeNode(ctx, node) 224 | w.push(ctx) 225 | 226 | ctx.length = len(node.children) 227 | ctx.needNewLine = !bytes.HasSuffix(node.content, []byte("\n")) 228 | 229 | for i, child := range node.children { 230 | ctx.index = i 231 | w.writeTree(ctx, child) 232 | } 233 | 234 | w.pop() 235 | } 236 | 237 | func (w *treeWriter) writeNode(ctx treeCtx, node *TreeWriter) { 238 | if ctx.needNewLine { 239 | w.writeString("\n") 240 | w.nextLine(ctx) 241 | } 242 | 243 | w.nextNode(ctx) 244 | i := 0 245 | 246 | forEachLine(node.content, func(line []byte) bool { 247 | if i != 0 { 248 | w.nextLine(ctx) 249 | } 250 | 251 | for _, symbol := range w.symbols { 252 | w.writeString(symbol) 253 | } 254 | 255 | w.write(line) 256 | i++ 257 | return true 258 | }) 259 | } 260 | 261 | func (w *treeWriter) writeString(s string) { 262 | if _, err := io.WriteString(w.writer, s); err != nil { 263 | panic(err) 264 | } 265 | } 266 | 267 | func (w *treeWriter) write(b []byte) { 268 | if _, err := w.writer.Write(b); err != nil { 269 | panic(err) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package textio 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | func TestTreeWriter(t *testing.T) { 13 | b := &bytes.Buffer{} 14 | w := NewTreeWriter(b) 15 | w.WriteString(".") 16 | 17 | w1 := NewTreeWriter(w) 18 | w2 := NewTreeWriter(w) 19 | w3 := NewTreeWriter(w) 20 | 21 | w1.WriteString("A") 22 | w2.WriteString("B\n(newline)\n") 23 | w3.WriteString("C") 24 | 25 | w21 := NewTreeWriter(w2) 26 | w21.WriteString("Hello World!") 27 | 28 | w.Close() 29 | 30 | const expected = `. 31 | ├── A 32 | ├── B 33 | │ (newline) 34 | │ └── Hello World! 35 | └── C` 36 | 37 | found := b.String() 38 | 39 | if expected != found { 40 | t.Error("content mismatch") 41 | t.Logf("expected: %s", expected) 42 | t.Logf("found: %s", found) 43 | } 44 | 45 | } 46 | 47 | func TestTreeWriterBase(t *testing.T) { 48 | b := &bytes.Buffer{} 49 | w := NewTreeWriter(b) 50 | w1 := NewTreeWriter(w) 51 | 52 | if x := Base(w1); x != w { 53 | t.Error("Base(w1) != w") 54 | } 55 | 56 | if x := Base(w); x != b { 57 | t.Error("Base(w) != b") 58 | } 59 | 60 | if x := Base(b); x != b { 61 | t.Error("Base(b) != b") 62 | } 63 | } 64 | 65 | func TestTreeWriterParent(t *testing.T) { 66 | b := &bytes.Buffer{} 67 | w := NewTreeWriter(b) 68 | w1 := NewTreeWriter(w) 69 | 70 | if x := Parent(w1); x != w { 71 | t.Error("Parent(w1) != w") 72 | } 73 | 74 | if x := Parent(w); x != b { 75 | t.Error("Parent(w) != w") 76 | } 77 | 78 | if x := Parent(b); x != b { 79 | t.Error("Parent(b) != b") 80 | } 81 | } 82 | 83 | func TestTreeWriterRoot(t *testing.T) { 84 | b := &bytes.Buffer{} 85 | w := NewTreeWriter(b) 86 | w1 := NewTreeWriter(w) 87 | 88 | if x := Root(w1); x != w { 89 | t.Error("Root(w1) != w") 90 | } 91 | 92 | if x := Root(w); x != w { 93 | t.Error("Root(w) != w") 94 | } 95 | 96 | if x := Root(b); x != b { 97 | t.Error("Root(b) != b") 98 | } 99 | } 100 | 101 | func ExampleNewTreeWriter() { 102 | var ls func(io.Writer, string) 103 | 104 | ls = func(w io.Writer, path string) { 105 | tree := NewTreeWriter(w) 106 | tree.WriteString(filepath.Base(path)) 107 | defer tree.Close() 108 | 109 | files, _ := ioutil.ReadDir(path) 110 | 111 | for _, f := range files { 112 | if f.Mode().IsDir() { 113 | ls(tree, filepath.Join(path, f.Name())) 114 | } 115 | } 116 | 117 | for _, f := range files { 118 | if !f.Mode().IsDir() { 119 | io.WriteString(NewTreeWriter(tree), f.Name()) 120 | } 121 | } 122 | } 123 | 124 | ls(os.Stdout, "examples") 125 | // Output: examples 126 | // ├── A 127 | // │ ├── 1 128 | // │ └── 2 129 | // └── message 130 | } 131 | --------------------------------------------------------------------------------