├── .gitignore ├── go.mod ├── go.sum ├── LICENSE ├── helpers.go ├── struct_test.go ├── README.md ├── treeprint_test.go ├── treeprint.go └── struct.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | .idea 3 | **/**.iml 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xlab/treeprint 2 | 3 | go 1.13 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2016 Maxim Kupriianov 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package treeprint 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | func isEmpty(v *reflect.Value) bool { 9 | switch v.Kind() { 10 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 11 | return v.Len() == 0 12 | case reflect.Bool: 13 | return !v.Bool() 14 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 15 | return v.Int() == 0 16 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 17 | return v.Uint() == 0 18 | case reflect.Float32, reflect.Float64: 19 | return v.Float() == 0 20 | case reflect.Interface, reflect.Ptr: 21 | return v.IsNil() 22 | } 23 | return false 24 | } 25 | 26 | func tagSpec(tag string) (name string, omit bool) { 27 | parts := strings.Split(tag, ",") 28 | if len(parts) < 2 { 29 | return tag, false 30 | } 31 | if parts[1] == "omitempty" { 32 | return parts[0], true 33 | } 34 | return parts[0], false 35 | } 36 | 37 | func filterTags(tag reflect.StructTag) string { 38 | tags := strings.Split(string(tag), " ") 39 | filtered := make([]string, 0, len(tags)) 40 | for i := range tags { 41 | if strings.HasPrefix(tags[i], "tree:") { 42 | continue 43 | } 44 | filtered = append(filtered, tags[i]) 45 | } 46 | return strings.Join(filtered, " ") 47 | } 48 | -------------------------------------------------------------------------------- /struct_test.go: -------------------------------------------------------------------------------- 1 | package treeprint 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type nameStruct struct { 11 | One string `json:"one" tree:"one"` 12 | Two int `tree:"two"` 13 | Three struct { 14 | SubOne []string 15 | SubTwo []interface{} 16 | SubThree struct { 17 | InnerOne *float64 `tree:"inner_one,omitempty"` 18 | InnerTwo *struct{} `tree:",omitempty"` 19 | InnerThree *float64 `tree:"inner_three"` 20 | } 21 | } 22 | } 23 | 24 | func TestFromStructName(t *testing.T) { 25 | assert := assert.New(t) 26 | 27 | tree, err := FromStruct(nameStruct{}, StructNameTree) 28 | assert.NoError(err) 29 | 30 | actual := tree.String() 31 | expected := `. 32 | ├── one 33 | ├── two 34 | └── Three 35 | ├── SubOne 36 | ├── SubTwo 37 | └── SubThree 38 | └── inner_three 39 | ` 40 | assert.Equal(expected, actual) 41 | } 42 | 43 | func TestFromStructTags(t *testing.T) { 44 | assert := assert.New(t) 45 | 46 | tree, err := FromStruct(nameStruct{}, StructTagTree) 47 | assert.NoError(err) 48 | 49 | actual := tree.String() 50 | expected := `. 51 | ├── [json:"one"] one 52 | ├── [] two 53 | └── [] Three 54 | ├── [] SubOne 55 | ├── [] SubTwo 56 | └── [] SubThree 57 | └── [] inner_three 58 | ` 59 | assert.Equal(expected, actual) 60 | } 61 | 62 | type typeStruct struct { 63 | One string `json:"one" tree:"one"` 64 | Two int `tree:"two"` 65 | Three subtypeStruct 66 | } 67 | 68 | type subtypeStruct struct { 69 | SubOne []string 70 | SubTwo []interface{} 71 | SubThree subsubTypeStruct 72 | } 73 | 74 | type subsubTypeStruct struct { 75 | InnerOne *float64 `tree:"inner_one,omitempty"` 76 | InnerTwo *struct{} `tree:",omitempty"` 77 | InnerThree *float64 `tree:"inner_three"` 78 | } 79 | 80 | func TestFromStructType(t *testing.T) { 81 | assert := assert.New(t) 82 | 83 | tree, err := FromStruct(typeStruct{}, StructTypeTree) 84 | assert.NoError(err) 85 | 86 | actual := tree.String() 87 | expected := `. 88 | ├── [string] one 89 | ├── [int] two 90 | └── [treeprint.subtypeStruct] Three 91 | ├── [[]string] SubOne 92 | ├── [[]interface {}] SubTwo 93 | └── [treeprint.subsubTypeStruct] SubThree 94 | └── [*float64] inner_three 95 | ` 96 | assert.Equal(expected, actual) 97 | } 98 | 99 | func TestFromStructTypeSize(t *testing.T) { 100 | assert := assert.New(t) 101 | 102 | tree, err := FromStruct(typeStruct{}, StructTypeSizeTree) 103 | assert.NoError(err) 104 | 105 | actual := tree.String() 106 | expected := `. 107 | ├── [16] one 108 | ├── [8] two 109 | └── [72] Three 110 | ├── [24] SubOne 111 | ├── [24] SubTwo 112 | └── [24] SubThree 113 | └── [8] inner_three 114 | ` 115 | assert.Equal(expected, actual) 116 | } 117 | 118 | type valueStruct struct { 119 | Name string 120 | Bio struct { 121 | Age int 122 | City string 123 | Meta interface{} 124 | } 125 | } 126 | 127 | func TestFromStructValue(t *testing.T) { 128 | assert := assert.New(t) 129 | 130 | val := valueStruct{ 131 | Name: "Max", 132 | } 133 | val.Bio.Age = 100 134 | val.Bio.City = "NYC" 135 | val.Bio.Meta = []byte("hello") 136 | tree, err := FromStruct(val, StructValueTree) 137 | assert.NoError(err) 138 | 139 | actual := tree.String() 140 | expected := `. 141 | ├── [Max] Name 142 | └── Bio 143 | ├── [100] Age 144 | ├── [NYC] City 145 | └── [[104 101 108 108 111]] Meta 146 | ` 147 | assert.Equal(expected, actual) 148 | } 149 | 150 | func TestFromStructWithMeta(t *testing.T) { 151 | assert := assert.New(t) 152 | 153 | val := valueStruct{ 154 | Name: "Max", 155 | } 156 | val.Bio.Age = 100 157 | val.Bio.City = "NYC" 158 | val.Bio.Meta = []byte("hello") 159 | tree, err := FromStructWithMeta(val, func(_ string, v interface{}) (string, bool) { 160 | return fmt.Sprintf("lol %T", v), true 161 | }) 162 | assert.NoError(err) 163 | 164 | actual := tree.String() 165 | expected := `. 166 | ├── [lol string] Name 167 | └── [lol struct { Age int; City string; Meta interface {} }] Bio 168 | ├── [lol int] Age 169 | ├── [lol string] City 170 | └── [lol []uint8] Meta 171 | ` 172 | assert.Equal(expected, actual) 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | treeprint [![GoDoc](https://godoc.org/github.com/xlab/treeprint?status.svg)](https://godoc.org/github.com/xlab/treeprint) ![test coverage](https://img.shields.io/badge/coverage-68.6%25-green.svg) 2 | ========= 3 | 4 | Package `treeprint` provides a simple ASCII tree composing tool. 5 | 6 | SYSTEME FIGURE 7 | 8 | If you are familiar with the [tree](http://mama.indstate.edu/users/ice/tree/) utility that is a recursive directory listing command that produces a depth indented listing of files, then you have the idea of what it would look like. 9 | 10 | On my system the command yields the following 11 | 12 | ``` 13 | $ tree 14 | . 15 | ├── LICENSE 16 | ├── README.md 17 | ├── treeprint.go 18 | └── treeprint_test.go 19 | 20 | 0 directories, 4 files 21 | ``` 22 | 23 | and I'd like to have the same format for my Go data structures when I print them. 24 | 25 | ## Installation 26 | 27 | ``` 28 | $ go get github.com/xlab/treeprint 29 | ``` 30 | 31 | ## Concept of work 32 | 33 | The general idea is that you initialise a new tree with `treeprint.New()` and then add nodes and 34 | branches into it. Use `AddNode()` when you want add a node on the same level as the target or 35 | use `AddBranch()` when you want to go a level deeper. So `tree.AddBranch().AddNode().AddNode()` would 36 | create a new level with two distinct nodes on it. So `tree.AddNode().AddNode()` is a flat thing and 37 | `tree.AddBranch().AddBranch().AddBranch()` is a high thing. Use `String()` or `Bytes()` on a branch 38 | to render a subtree, or use it on the root to print the whole tree. 39 | 40 | The utility will yield Unicode-friendly trees. The output is predictable and there is no platform-dependent exceptions, so if you have issues with displaying the tree in the console, all platform-related transformations can be done after the tree has been rendered: [an example](https://github.com/xlab/treeprint/issues/2#issuecomment-324944141) for Asian locales. 41 | 42 | See also [this fork](https://github.com/apstndb/treeprint) that introduces options to set custom node symbols before yielding the tree. 43 | 44 | ## Use cases 45 | 46 | ### When you want to render a complex data structure: 47 | 48 | ```go 49 | func main() { 50 | // to add a custom root name use `treeprint.NewWithRoot()` instead 51 | tree := treeprint.New() 52 | 53 | // create a new branch in the root 54 | one := tree.AddBranch("one") 55 | 56 | // add some nodes 57 | one.AddNode("subnode1").AddNode("subnode2") 58 | 59 | // create a new sub-branch 60 | one.AddBranch("two"). 61 | AddNode("subnode1").AddNode("subnode2"). // add some nodes 62 | AddBranch("three"). // add a new sub-branch 63 | AddNode("subnode1").AddNode("subnode2") // add some nodes too 64 | 65 | // add one more node that should surround the inner branch 66 | one.AddNode("subnode3") 67 | 68 | // add a new node to the root 69 | tree.AddNode("outernode") 70 | 71 | fmt.Println(tree.String()) 72 | } 73 | ``` 74 | 75 | Will give you: 76 | 77 | ``` 78 | . 79 | ├── one 80 | │   ├── subnode1 81 | │   ├── subnode2 82 | │   ├── two 83 | │   │   ├── subnode1 84 | │   │   ├── subnode2 85 | │   │   └── three 86 | │   │   ├── subnode1 87 | │   │   └── subnode2 88 | │   └── subnode3 89 | └── outernode 90 | ``` 91 | 92 | ### Another case, when you have to make a tree where any leaf may have some meta-data (as `tree` is capable of it): 93 | 94 | ```go 95 | func main { 96 | // to add a custom root name use `treeprint.NewWithRoot()` instead 97 | tree := treeprint.New() 98 | 99 | tree.AddNode("Dockerfile") 100 | tree.AddNode("Makefile") 101 | tree.AddNode("aws.sh") 102 | tree.AddMetaBranch(" 204", "bin"). 103 | AddNode("dbmaker").AddNode("someserver").AddNode("testtool") 104 | tree.AddMetaBranch(" 374", "deploy"). 105 | AddNode("Makefile").AddNode("bootstrap.sh") 106 | tree.AddMetaNode("122K", "testtool.a") 107 | 108 | fmt.Println(tree.String()) 109 | } 110 | ``` 111 | 112 | Output: 113 | 114 | ``` 115 | . 116 | ├── Dockerfile 117 | ├── Makefile 118 | ├── aws.sh 119 | ├── [ 204] bin 120 | │   ├── dbmaker 121 | │   ├── someserver 122 | │   └── testtool 123 | ├── [ 374] deploy 124 | │   ├── Makefile 125 | │   └── bootstrap.sh 126 | └── [122K] testtool.a 127 | ``` 128 | 129 | ### Iterating over the tree nodes 130 | 131 | ```go 132 | tree := New() 133 | 134 | one := tree.AddBranch("one") 135 | one.AddNode("one-subnode1").AddNode("one-subnode2") 136 | one.AddBranch("two").AddNode("two-subnode1").AddNode("two-subnode2"). 137 | AddBranch("three").AddNode("three-subnode1").AddNode("three-subnode2") 138 | tree.AddNode("outernode") 139 | 140 | // if you need to iterate over the whole tree 141 | // call `VisitAll` from your top root node. 142 | tree.VisitAll(func(item *node) { 143 | if len(item.Nodes) > 0 { 144 | // branch nodes 145 | fmt.Println(item.Value) // will output one, two, three 146 | } else { 147 | // leaf nodes 148 | fmt.Println(item.Value) // will output one-*, two-*, three-* and outernode 149 | } 150 | }) 151 | 152 | ``` 153 | Yay! So it works. 154 | 155 | ## License 156 | MIT 157 | -------------------------------------------------------------------------------- /treeprint_test.go: -------------------------------------------------------------------------------- 1 | package treeprint 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestZeroNodesWithRoot(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tree := NewWithRoot("mytree") 13 | actual := tree.String() 14 | expected := "mytree\n" 15 | assert.Equal(expected, actual) 16 | } 17 | 18 | func TestOneNode(t *testing.T) { 19 | assert := assert.New(t) 20 | 21 | tree := New() 22 | tree.AddNode("hello") 23 | actual := tree.String() 24 | expected := `. 25 | └── hello 26 | ` 27 | assert.Equal(expected, actual) 28 | } 29 | 30 | func TestOneNodeWithRoot(t *testing.T) { 31 | assert := assert.New(t) 32 | 33 | tree := NewWithRoot("mytree") 34 | tree.AddNode("hello") 35 | actual := tree.String() 36 | expected := `mytree 37 | └── hello 38 | ` 39 | assert.Equal(expected, actual) 40 | } 41 | 42 | func TestMetaNode(t *testing.T) { 43 | assert := assert.New(t) 44 | 45 | tree := New() 46 | tree.AddMetaNode(123, "hello") 47 | tree.AddMetaNode([]struct{}{}, "world") 48 | actual := tree.String() 49 | expected := `. 50 | ├── [123] hello 51 | └── [[]] world 52 | ` 53 | assert.Equal(expected, actual) 54 | } 55 | 56 | func TestTwoNodes(t *testing.T) { 57 | assert := assert.New(t) 58 | 59 | tree := New() 60 | tree.AddNode("hello") 61 | tree.AddNode("world") 62 | actual := tree.String() 63 | expected := `. 64 | ├── hello 65 | └── world 66 | ` 67 | assert.Equal(expected, actual) 68 | } 69 | 70 | func TestLevel(t *testing.T) { 71 | assert := assert.New(t) 72 | 73 | tree := New() 74 | tree.AddBranch("hello").AddNode("my friend").AddNode("lol") 75 | tree.AddNode("world") 76 | actual := tree.String() 77 | expected := `. 78 | ├── hello 79 | │ ├── my friend 80 | │ └── lol 81 | └── world 82 | ` 83 | assert.Equal(expected, actual) 84 | } 85 | 86 | func TestNamedRoot(t *testing.T) { 87 | assert := assert.New(t) 88 | 89 | tree := New() 90 | tree.AddBranch("hello").AddNode("my friend").AddNode("lol") 91 | tree.AddNode("world") 92 | tree.SetValue("friends") 93 | actual := tree.String() 94 | expected := `friends 95 | ├── hello 96 | │ ├── my friend 97 | │ └── lol 98 | └── world 99 | ` 100 | assert.Equal(expected, actual) 101 | } 102 | 103 | func TestDeepLevel(t *testing.T) { 104 | assert := assert.New(t) 105 | 106 | tree := New() 107 | one := tree.AddBranch("one") 108 | one.AddNode("subnode1").AddNode("subnode2") 109 | one.AddBranch("two"). 110 | AddNode("subnode1").AddNode("subnode2"). 111 | AddBranch("three"). 112 | AddNode("subnode1").AddNode("subnode2") 113 | one.AddNode("subnode3") 114 | tree.AddNode("outernode") 115 | 116 | actual := tree.String() 117 | expected := `. 118 | ├── one 119 | │ ├── subnode1 120 | │ ├── subnode2 121 | │ ├── two 122 | │ │ ├── subnode1 123 | │ │ ├── subnode2 124 | │ │ └── three 125 | │ │ ├── subnode1 126 | │ │ └── subnode2 127 | │ └── subnode3 128 | └── outernode 129 | ` 130 | assert.Equal(expected, actual) 131 | } 132 | 133 | func TestComplex(t *testing.T) { 134 | assert := assert.New(t) 135 | 136 | tree := New() 137 | tree.AddNode("Dockerfile") 138 | tree.AddNode("Makefile") 139 | tree.AddNode("aws.sh") 140 | tree.AddMetaBranch(" 204", "bin"). 141 | AddNode("dbmaker").AddNode("someserver").AddNode("testtool") 142 | tree.AddMetaBranch(" 374", "deploy"). 143 | AddNode("Makefile").AddNode("bootstrap.sh") 144 | tree.AddMetaNode("122K", "testtool.a") 145 | 146 | actual := tree.String() 147 | expected := `. 148 | ├── Dockerfile 149 | ├── Makefile 150 | ├── aws.sh 151 | ├── [ 204] bin 152 | │ ├── dbmaker 153 | │ ├── someserver 154 | │ └── testtool 155 | ├── [ 374] deploy 156 | │ ├── Makefile 157 | │ └── bootstrap.sh 158 | └── [122K] testtool.a 159 | ` 160 | assert.Equal(expected, actual) 161 | } 162 | 163 | func TestIndirectOrder(t *testing.T) { 164 | assert := assert.New(t) 165 | 166 | tree := New() 167 | tree.AddBranch("one").AddNode("two") 168 | foo := tree.AddBranch("foo") 169 | foo.AddBranch("bar").AddNode("a").AddNode("b").AddNode("c") 170 | foo.AddNode("end") 171 | 172 | actual := tree.String() 173 | expected := `. 174 | ├── one 175 | │ └── two 176 | └── foo 177 | ├── bar 178 | │ ├── a 179 | │ ├── b 180 | │ └── c 181 | └── end 182 | ` 183 | assert.Equal(expected, actual) 184 | } 185 | 186 | func TestEdgeTypeAndIndent(t *testing.T) { 187 | assert := assert.New(t) 188 | 189 | // Restore to the original values 190 | defer func(link, mid, end EdgeType, indent int) { 191 | EdgeTypeLink = link 192 | EdgeTypeMid = mid 193 | EdgeTypeEnd = end 194 | IndentSize = indent 195 | }(EdgeTypeLink, EdgeTypeMid, EdgeTypeEnd, IndentSize) 196 | 197 | EdgeTypeLink = "|" 198 | EdgeTypeMid = "+-" 199 | EdgeTypeEnd = "+-" 200 | IndentSize = 2 201 | 202 | tree := New() 203 | tree.AddBranch("one").AddNode("two") 204 | foo := tree.AddBranch("foo") 205 | foo.AddBranch("bar").AddNode("a").AddNode("b").AddNode("c") 206 | foo.AddNode("end") 207 | 208 | actual := tree.String() 209 | expected := `. 210 | +- one 211 | | +- two 212 | +- foo 213 | +- bar 214 | | +- a 215 | | +- b 216 | | +- c 217 | +- end 218 | ` 219 | assert.Equal(expected, actual) 220 | } 221 | 222 | func TestRelationships(t *testing.T) { 223 | assert := assert.New(t) 224 | 225 | tree := New() 226 | tree.AddBranch("one").AddNode("two") 227 | foo := tree.AddBranch("foo") 228 | foo.AddBranch("bar").AddNode("a").AddNode("b").AddNode("c") 229 | foo.AddNode("end") 230 | 231 | treeNode := tree.(*Node) 232 | 233 | assert.Nil(treeNode.Root) 234 | assert.Len(treeNode.Nodes, 2) 235 | assert.Equal(treeNode, treeNode.Nodes[0].Root) 236 | assert.Equal(treeNode.Nodes[0], treeNode.Nodes[0].Nodes[0].Root) 237 | } 238 | 239 | func TestMultiline(t *testing.T) { 240 | assert := assert.New(t) 241 | 242 | multi1 := `I am 243 | a multiline 244 | value` 245 | 246 | multi2 := `I have 247 | many 248 | 249 | 250 | empty lines` 251 | 252 | multi3 := `I am another 253 | multiple 254 | lines value` 255 | 256 | tree := New() 257 | tree.AddBranch("one").AddMetaNode("meta", multi1) 258 | tree.AddBranch("two") 259 | foo := tree.AddBranch("foo") 260 | foo.AddBranch("bar").AddNode("a").AddNode(multi2).AddNode("c") 261 | foo.AddBranch(multi3) 262 | 263 | actual := tree.String() 264 | expected := `. 265 | ├── one 266 | │ └── [meta] I am 267 | │ a multiline 268 | │ value 269 | ├── two 270 | └── foo 271 | ├── bar 272 | │ ├── a 273 | │ ├── I have 274 | │ │ many 275 | │ │ 276 | │ │ 277 | │ │ empty lines 278 | │ └── c 279 | └── I am another 280 | multiple 281 | lines value 282 | ` 283 | 284 | assert.Equal(expected, actual) 285 | } 286 | 287 | func TestVisitAll(t *testing.T) { 288 | 289 | tree := New() 290 | one := tree.AddBranch("one") 291 | one.AddNode("one-subnode1").AddNode("one-subnode2") 292 | one.AddBranch("two").AddNode("two-subnode1").AddNode("two-subnode2"). 293 | AddBranch("three").AddNode("three-subnode1").AddNode("three-subnode2") 294 | tree.AddNode("outernode") 295 | 296 | var visitedNodeValues []Value 297 | expectedNodeValues := []Value{ 298 | "one", 299 | "one-subnode1", 300 | "one-subnode2", 301 | "two", 302 | "two-subnode1", 303 | "two-subnode2", 304 | "three", 305 | "three-subnode1", 306 | "three-subnode2", 307 | "outernode", 308 | } 309 | 310 | tree.VisitAll(func(item *Node) { 311 | visitedNodeValues = append(visitedNodeValues, item.Value) 312 | }) 313 | 314 | assert := assert.New(t) 315 | assert.Equal(expectedNodeValues, visitedNodeValues) 316 | 317 | } 318 | -------------------------------------------------------------------------------- /treeprint.go: -------------------------------------------------------------------------------- 1 | // Package treeprint provides a simple ASCII tree composing tool. 2 | package treeprint 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | // Value defines any value 13 | type Value interface{} 14 | 15 | // MetaValue defines any meta value 16 | type MetaValue interface{} 17 | 18 | // NodeVisitor function type for iterating over nodes 19 | type NodeVisitor func(item *Node) 20 | 21 | // Tree represents a tree structure with leaf-nodes and branch-nodes. 22 | type Tree interface { 23 | // AddNode adds a new Node to a branch. 24 | AddNode(v Value) Tree 25 | // AddMetaNode adds a new Node with meta value provided to a branch. 26 | AddMetaNode(meta MetaValue, v Value) Tree 27 | // AddBranch adds a new branch Node (a level deeper). 28 | AddBranch(v Value) Tree 29 | // AddMetaBranch adds a new branch Node (a level deeper) with meta value provided. 30 | AddMetaBranch(meta MetaValue, v Value) Tree 31 | // Branch converts a leaf-Node to a branch-Node, 32 | // applying this on a branch-Node does no effect. 33 | Branch() Tree 34 | // FindByMeta finds a Node whose meta value matches the provided one by reflect.DeepEqual, 35 | // returns nil if not found. 36 | FindByMeta(meta MetaValue) Tree 37 | // FindByValue finds a Node whose value matches the provided one by reflect.DeepEqual, 38 | // returns nil if not found. 39 | FindByValue(value Value) Tree 40 | // returns the last Node of a tree 41 | FindLastNode() Tree 42 | // String renders the tree or subtree as a string. 43 | String() string 44 | // Bytes renders the tree or subtree as byteslice. 45 | Bytes() []byte 46 | 47 | SetValue(value Value) 48 | SetMetaValue(meta MetaValue) 49 | 50 | // VisitAll iterates over the tree, branches and nodes. 51 | // If need to iterate over the whole tree, use the root Node. 52 | // Note this method uses a breadth-first approach. 53 | VisitAll(fn NodeVisitor) 54 | } 55 | 56 | type Node struct { 57 | Root *Node 58 | Meta MetaValue 59 | Value Value 60 | Nodes []*Node 61 | } 62 | 63 | func (n *Node) FindLastNode() Tree { 64 | ns := n.Nodes 65 | if len(ns) == 0 { 66 | return nil 67 | } 68 | return ns[len(ns)-1] 69 | } 70 | 71 | func (n *Node) AddNode(v Value) Tree { 72 | n.Nodes = append(n.Nodes, &Node{ 73 | Root: n, 74 | Value: v, 75 | }) 76 | return n 77 | } 78 | 79 | func (n *Node) AddMetaNode(meta MetaValue, v Value) Tree { 80 | n.Nodes = append(n.Nodes, &Node{ 81 | Root: n, 82 | Meta: meta, 83 | Value: v, 84 | }) 85 | return n 86 | } 87 | 88 | func (n *Node) AddBranch(v Value) Tree { 89 | branch := &Node{ 90 | Root: n, 91 | Value: v, 92 | } 93 | n.Nodes = append(n.Nodes, branch) 94 | return branch 95 | } 96 | 97 | func (n *Node) AddMetaBranch(meta MetaValue, v Value) Tree { 98 | branch := &Node{ 99 | Root: n, 100 | Meta: meta, 101 | Value: v, 102 | } 103 | n.Nodes = append(n.Nodes, branch) 104 | return branch 105 | } 106 | 107 | func (n *Node) Branch() Tree { 108 | n.Root = nil 109 | return n 110 | } 111 | 112 | func (n *Node) FindByMeta(meta MetaValue) Tree { 113 | for _, node := range n.Nodes { 114 | if reflect.DeepEqual(node.Meta, meta) { 115 | return node 116 | } 117 | if v := node.FindByMeta(meta); v != nil { 118 | return v 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | func (n *Node) FindByValue(value Value) Tree { 125 | for _, node := range n.Nodes { 126 | if reflect.DeepEqual(node.Value, value) { 127 | return node 128 | } 129 | if v := node.FindByMeta(value); v != nil { 130 | return v 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | func (n *Node) Bytes() []byte { 137 | buf := new(bytes.Buffer) 138 | level := 0 139 | var levelsEnded []int 140 | if n.Root == nil { 141 | if n.Meta != nil { 142 | buf.WriteString(fmt.Sprintf("[%v] %v", n.Meta, n.Value)) 143 | } else { 144 | buf.WriteString(fmt.Sprintf("%v", n.Value)) 145 | } 146 | buf.WriteByte('\n') 147 | } else { 148 | edge := EdgeTypeMid 149 | if len(n.Nodes) == 0 { 150 | edge = EdgeTypeEnd 151 | levelsEnded = append(levelsEnded, level) 152 | } 153 | printValues(buf, 0, levelsEnded, edge, n) 154 | } 155 | if len(n.Nodes) > 0 { 156 | printNodes(buf, level, levelsEnded, n.Nodes) 157 | } 158 | return buf.Bytes() 159 | } 160 | 161 | func (n *Node) String() string { 162 | return string(n.Bytes()) 163 | } 164 | 165 | func (n *Node) SetValue(value Value) { 166 | n.Value = value 167 | } 168 | 169 | func (n *Node) SetMetaValue(meta MetaValue) { 170 | n.Meta = meta 171 | } 172 | 173 | func (n *Node) VisitAll(fn NodeVisitor) { 174 | for _, node := range n.Nodes { 175 | fn(node) 176 | 177 | if len(node.Nodes) > 0 { 178 | node.VisitAll(fn) 179 | continue 180 | } 181 | } 182 | } 183 | 184 | func printNodes(wr io.Writer, 185 | level int, levelsEnded []int, nodes []*Node) { 186 | 187 | for i, node := range nodes { 188 | edge := EdgeTypeMid 189 | if i == len(nodes)-1 { 190 | levelsEnded = append(levelsEnded, level) 191 | edge = EdgeTypeEnd 192 | } 193 | printValues(wr, level, levelsEnded, edge, node) 194 | if len(node.Nodes) > 0 { 195 | printNodes(wr, level+1, levelsEnded, node.Nodes) 196 | } 197 | } 198 | } 199 | 200 | func printValues(wr io.Writer, 201 | level int, levelsEnded []int, edge EdgeType, node *Node) { 202 | 203 | for i := 0; i < level; i++ { 204 | if isEnded(levelsEnded, i) { 205 | fmt.Fprint(wr, strings.Repeat(" ", IndentSize+1)) 206 | continue 207 | } 208 | fmt.Fprintf(wr, "%s%s", EdgeTypeLink, strings.Repeat(" ", IndentSize)) 209 | } 210 | 211 | val := renderValue(level, node) 212 | meta := node.Meta 213 | 214 | if meta != nil { 215 | fmt.Fprintf(wr, "%s [%v] %v\n", edge, meta, val) 216 | return 217 | } 218 | fmt.Fprintf(wr, "%s %v\n", edge, val) 219 | } 220 | 221 | func isEnded(levelsEnded []int, level int) bool { 222 | for _, l := range levelsEnded { 223 | if l == level { 224 | return true 225 | } 226 | } 227 | return false 228 | } 229 | 230 | func renderValue(level int, node *Node) Value { 231 | lines := strings.Split(fmt.Sprintf("%v", node.Value), "\n") 232 | 233 | // If value does not contain multiple lines, return itself. 234 | if len(lines) < 2 { 235 | return node.Value 236 | } 237 | 238 | // If value contains multiple lines, 239 | // generate a padding and prefix each line with it. 240 | pad := padding(level, node) 241 | 242 | for i := 1; i < len(lines); i++ { 243 | lines[i] = fmt.Sprintf("%s%s", pad, lines[i]) 244 | } 245 | 246 | return strings.Join(lines, "\n") 247 | } 248 | 249 | // padding returns a padding for the multiline values with correctly placed link edges. 250 | // It is generated by traversing the tree upwards (from leaf to the root of the tree) 251 | // and, on each level, checking if the Node the last one of its siblings. 252 | // If a Node is the last one, the padding on that level should be empty (there's nothing to link to below it). 253 | // If a Node is not the last one, the padding on that level should be the link edge so the sibling below is correctly connected. 254 | func padding(level int, node *Node) string { 255 | links := make([]string, level+1) 256 | 257 | for node.Root != nil { 258 | if isLast(node) { 259 | links[level] = strings.Repeat(" ", IndentSize+1) 260 | } else { 261 | links[level] = fmt.Sprintf("%s%s", EdgeTypeLink, strings.Repeat(" ", IndentSize)) 262 | } 263 | level-- 264 | node = node.Root 265 | } 266 | 267 | return strings.Join(links, "") 268 | } 269 | 270 | // isLast checks if the Node is the last one in the slice of its parent children 271 | func isLast(n *Node) bool { 272 | return n == n.Root.FindLastNode() 273 | } 274 | 275 | type EdgeType string 276 | 277 | var ( 278 | EdgeTypeLink EdgeType = "│" 279 | EdgeTypeMid EdgeType = "├──" 280 | EdgeTypeEnd EdgeType = "└──" 281 | ) 282 | 283 | // IndentSize is the number of spaces per tree level. 284 | var IndentSize = 3 285 | 286 | // New Generates new tree 287 | func New() Tree { 288 | return &Node{Value: "."} 289 | } 290 | 291 | // NewWithRoot Generates new tree with the given root value 292 | func NewWithRoot(root Value) Tree { 293 | return &Node{Value: root} 294 | } 295 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | package treeprint 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type StructTreeOption int 10 | 11 | const ( 12 | StructNameTree StructTreeOption = iota 13 | StructValueTree 14 | StructTagTree 15 | StructTypeTree 16 | StructTypeSizeTree 17 | ) 18 | 19 | func FromStruct(v interface{}, opt ...StructTreeOption) (Tree, error) { 20 | var treeOpt StructTreeOption 21 | if len(opt) > 0 { 22 | treeOpt = opt[0] 23 | } 24 | switch treeOpt { 25 | case StructNameTree: 26 | tree := New() 27 | err := nameTree(tree, v) 28 | return tree, err 29 | case StructValueTree: 30 | tree := New() 31 | err := valueTree(tree, v) 32 | return tree, err 33 | case StructTagTree: 34 | tree := New() 35 | err := tagTree(tree, v) 36 | return tree, err 37 | case StructTypeTree: 38 | tree := New() 39 | err := typeTree(tree, v) 40 | return tree, err 41 | case StructTypeSizeTree: 42 | tree := New() 43 | err := typeSizeTree(tree, v) 44 | return tree, err 45 | default: 46 | err := fmt.Errorf("treeprint: invalid StructTreeOption %v", treeOpt) 47 | return nil, err 48 | } 49 | } 50 | 51 | type FmtFunc func(name string, v interface{}) (string, bool) 52 | 53 | func FromStructWithMeta(v interface{}, fmtFunc FmtFunc) (Tree, error) { 54 | if fmtFunc == nil { 55 | tree := New() 56 | err := nameTree(tree, v) 57 | return tree, err 58 | } 59 | tree := New() 60 | err := metaTree(tree, v, fmtFunc) 61 | return tree, err 62 | } 63 | 64 | func Repr(v interface{}) string { 65 | tree := New() 66 | vType := reflect.TypeOf(v) 67 | vValue := reflect.ValueOf(v) 68 | _, val, isStruct := getValue(vType, &vValue) 69 | if !isStruct { 70 | return fmt.Sprintf("%+v", val.Interface()) 71 | } 72 | err := valueTree(tree, val.Interface()) 73 | if err != nil { 74 | return err.Error() 75 | } 76 | return tree.String() 77 | } 78 | 79 | func nameTree(tree Tree, v interface{}) error { 80 | typ, val, err := checkType(v) 81 | if err != nil { 82 | return err 83 | } 84 | fields := typ.NumField() 85 | for i := 0; i < fields; i++ { 86 | field := typ.Field(i) 87 | fieldValue := val.Field(i) 88 | name, skip, omit := getMeta(field.Name, field.Tag) 89 | if skip || omit && isEmpty(&fieldValue) { 90 | continue 91 | } 92 | typ, val, isStruct := getValue(field.Type, &fieldValue) 93 | if !isStruct { 94 | tree.AddNode(name) 95 | continue 96 | } else if subNum := typ.NumField(); subNum == 0 { 97 | tree.AddNode(name) 98 | continue 99 | } 100 | branch := tree.AddBranch(name) 101 | if err := nameTree(branch, val.Interface()); err != nil { 102 | err := fmt.Errorf("%v on struct branch %s", err, name) 103 | return err 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | func getMeta(fieldName string, tag reflect.StructTag) (name string, skip, omit bool) { 110 | if tagStr := tag.Get("tree"); len(tagStr) > 0 { 111 | name, omit = tagSpec(tagStr) 112 | } 113 | if name == "-" { 114 | return fieldName, true, omit 115 | } 116 | if len(name) == 0 { 117 | name = fieldName 118 | } else if trimmed := strings.TrimSpace(name); len(trimmed) == 0 { 119 | name = fieldName 120 | } 121 | return 122 | } 123 | 124 | func valueTree(tree Tree, v interface{}) error { 125 | typ, val, err := checkType(v) 126 | if err != nil { 127 | return err 128 | } 129 | fields := typ.NumField() 130 | for i := 0; i < fields; i++ { 131 | field := typ.Field(i) 132 | fieldValue := val.Field(i) 133 | name, skip, omit := getMeta(field.Name, field.Tag) 134 | if skip || omit && isEmpty(&fieldValue) { 135 | continue 136 | } 137 | typ, val, isStruct := getValue(field.Type, &fieldValue) 138 | if !isStruct { 139 | tree.AddMetaNode(val.Interface(), name) 140 | continue 141 | } else if subNum := typ.NumField(); subNum == 0 { 142 | tree.AddMetaNode(val.Interface(), name) 143 | continue 144 | } 145 | branch := tree.AddBranch(name) 146 | if err := valueTree(branch, val.Interface()); err != nil { 147 | err := fmt.Errorf("%v on struct branch %s", err, name) 148 | return err 149 | } 150 | } 151 | return nil 152 | } 153 | 154 | func tagTree(tree Tree, v interface{}) error { 155 | typ, val, err := checkType(v) 156 | if err != nil { 157 | return err 158 | } 159 | fields := typ.NumField() 160 | for i := 0; i < fields; i++ { 161 | field := typ.Field(i) 162 | fieldValue := val.Field(i) 163 | name, skip, omit := getMeta(field.Name, field.Tag) 164 | if skip || omit && isEmpty(&fieldValue) { 165 | continue 166 | } 167 | filteredTag := filterTags(field.Tag) 168 | typ, val, isStruct := getValue(field.Type, &fieldValue) 169 | if !isStruct { 170 | tree.AddMetaNode(filteredTag, name) 171 | continue 172 | } else if subNum := typ.NumField(); subNum == 0 { 173 | tree.AddMetaNode(filteredTag, name) 174 | continue 175 | } 176 | branch := tree.AddMetaBranch(filteredTag, name) 177 | if err := tagTree(branch, val.Interface()); err != nil { 178 | err := fmt.Errorf("%v on struct branch %s", err, name) 179 | return err 180 | } 181 | } 182 | return nil 183 | } 184 | 185 | func typeTree(tree Tree, v interface{}) error { 186 | typ, val, err := checkType(v) 187 | if err != nil { 188 | return err 189 | } 190 | fields := typ.NumField() 191 | for i := 0; i < fields; i++ { 192 | field := typ.Field(i) 193 | fieldValue := val.Field(i) 194 | name, skip, omit := getMeta(field.Name, field.Tag) 195 | if skip || omit && isEmpty(&fieldValue) { 196 | continue 197 | } 198 | typ, val, isStruct := getValue(field.Type, &fieldValue) 199 | typename := fmt.Sprintf("%T", val.Interface()) 200 | if !isStruct { 201 | tree.AddMetaNode(typename, name) 202 | continue 203 | } else if subNum := typ.NumField(); subNum == 0 { 204 | tree.AddMetaNode(typename, name) 205 | continue 206 | } 207 | branch := tree.AddMetaBranch(typename, name) 208 | if err := typeTree(branch, val.Interface()); err != nil { 209 | err := fmt.Errorf("%v on struct branch %s", err, name) 210 | return err 211 | } 212 | } 213 | return nil 214 | } 215 | 216 | func typeSizeTree(tree Tree, v interface{}) error { 217 | typ, val, err := checkType(v) 218 | if err != nil { 219 | return err 220 | } 221 | fields := typ.NumField() 222 | for i := 0; i < fields; i++ { 223 | field := typ.Field(i) 224 | fieldValue := val.Field(i) 225 | name, skip, omit := getMeta(field.Name, field.Tag) 226 | if skip || omit && isEmpty(&fieldValue) { 227 | continue 228 | } 229 | typ, val, isStruct := getValue(field.Type, &fieldValue) 230 | typesize := typ.Size() 231 | if !isStruct { 232 | tree.AddMetaNode(typesize, name) 233 | continue 234 | } else if subNum := typ.NumField(); subNum == 0 { 235 | tree.AddMetaNode(typesize, name) 236 | continue 237 | } 238 | branch := tree.AddMetaBranch(typesize, name) 239 | if err := typeSizeTree(branch, val.Interface()); err != nil { 240 | err := fmt.Errorf("%v on struct branch %s", err, name) 241 | return err 242 | } 243 | } 244 | return nil 245 | } 246 | 247 | func metaTree(tree Tree, v interface{}, fmtFunc FmtFunc) error { 248 | typ, val, err := checkType(v) 249 | if err != nil { 250 | return err 251 | } 252 | fields := typ.NumField() 253 | for i := 0; i < fields; i++ { 254 | field := typ.Field(i) 255 | fieldValue := val.Field(i) 256 | name, skip, omit := getMeta(field.Name, field.Tag) 257 | if skip || omit && isEmpty(&fieldValue) { 258 | continue 259 | } 260 | typ, val, isStruct := getValue(field.Type, &fieldValue) 261 | formatted, show := fmtFunc(name, val.Interface()) 262 | if !isStruct { 263 | if show { 264 | tree.AddMetaNode(formatted, name) 265 | continue 266 | } 267 | tree.AddNode(name) 268 | continue 269 | } else if subNum := typ.NumField(); subNum == 0 { 270 | if show { 271 | tree.AddMetaNode(formatted, name) 272 | continue 273 | } 274 | tree.AddNode(name) 275 | continue 276 | } 277 | var branch Tree 278 | if show { 279 | branch = tree.AddMetaBranch(formatted, name) 280 | } else { 281 | branch = tree.AddBranch(name) 282 | } 283 | if err := metaTree(branch, val.Interface(), fmtFunc); err != nil { 284 | err := fmt.Errorf("%v on struct branch %s", err, name) 285 | return err 286 | } 287 | } 288 | return nil 289 | } 290 | 291 | func getValue(typ reflect.Type, val *reflect.Value) (reflect.Type, *reflect.Value, bool) { 292 | switch typ.Kind() { 293 | case reflect.Ptr: 294 | typ = typ.Elem() 295 | if typ.Kind() == reflect.Struct { 296 | elem := val.Elem() 297 | return typ, &elem, true 298 | } 299 | case reflect.Struct: 300 | return typ, val, true 301 | } 302 | return typ, val, false 303 | } 304 | 305 | func checkType(v interface{}) (reflect.Type, *reflect.Value, error) { 306 | typ := reflect.TypeOf(v) 307 | val := reflect.ValueOf(v) 308 | switch typ.Kind() { 309 | case reflect.Ptr: 310 | typ = typ.Elem() 311 | if typ.Kind() != reflect.Struct { 312 | err := fmt.Errorf("treeprint: %T is not a struct we could work with", v) 313 | return nil, nil, err 314 | } 315 | val = val.Elem() 316 | case reflect.Struct: 317 | default: 318 | err := fmt.Errorf("treeprint: %T is not a struct we could work with", v) 319 | return nil, nil, err 320 | } 321 | return typ, &val, nil 322 | } 323 | --------------------------------------------------------------------------------