Want to output a tree in Go?

Ochi Daiki
6 min readOct 21, 2023

--

I am sure that you have often used the tree command to output a tree of directories.

But have you ever wanted to output a tree of something other than your local directory?

There is a Go package that you should know about, and that is https://github.com/ddddddO/gtree . This is the perfect library to easily output a tree.

Here is a simple Go program (main.go) that outputs a tree from the find command.

package main

import (
"bufio"
"fmt"
"os"
"strings"

"github.com/ddddddO/gtree"
)

// Example:
// $ find . -type d -name .git -prune -o -type f -print
// ./config.go
// ./node_generator_test.go
// ./example/like_cli/adapter/indentation.go
// ./example/like_cli/adapter/executor.go
// ./example/like_cli/main.go
// ./example/find_pipe_programmable-gtree/main.go
// ...
// $ find . -type d -name .git -prune -o -type f -print | go run main.go
// << See "Output:" below. >>
func main() {
var (
root *gtree.Node
node *gtree.Node
)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text() // e.g.) "./example/find_pipe_programmable-gtree/main.go"
splited := strings.Split(line, "/") // e.g.) [. example find_pipe_programmable-gtree main.go]

for i, s := range splited {
if root == nil {
root = gtree.NewRoot(s) // s := "."
node = root
continue
}
if i == 0 {
continue
}

tmp := node.Add(s)
node = tmp
}
node = root
}

if err := gtree.OutputProgrammably(os.Stdout, root); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Output:
// .
// ├── config.go
// ├── node_generator_test.go
// ├── example
// │ ├── like_cli
// │ │ ├── adapter
// │ │ │ ├── indentation.go
// │ │ │ └── executor.go
// │ │ └── main.go
// │ ├── find_pipe_programmable-gtree
// │ │ └── main.go
// │ ├── go-list_pipe_programmable-gtree
// │ │ └── main.go
// │ └── programmable
// │ └── main.go
// ├── file_considerer.go
// ├── node.go
// ├── node_generator.go
// ├── .gitignore
// ...
}

I was able to output a tree with the above program.
The program I introduced here was just a utility to display a tree of directories on the local PC after all.

However, do not be discouraged.

Developers who are not me have created CLI tools to output a tree of Amazon S3 and GCS buckets! You can find them in the following repositories.

I had no idea of displaying a tree of cloud storage. Also, the `aws s3` and `gcloud storage` commands do not have the functionality to display a tree.
I am sure you will be happy to use these CLI tools.

I got very good inspiration from these CLI tool developers. I am also very happy that they are using the gtree package!

If you want to tree output something, you may find https://github.com/ddddddO/gtree useful.

Thanks for reading!

Add.

The above was done by defining the tree in code and outputting it with the function `gtree.OutputProgrammably`.
If you want to modify the tree in some way (e.g., use an external library), you may want to use the `gtree.WalkProgrammably` function. Here is a sample code.

package main

import (
"fmt"
"os"

"github.com/ddddddO/gtree"
)

func main() {
root := gtree.NewRoot("root")
root.Add("child 1").Add("child 2").Add("child 3")
root.Add("child 5")
root.Add("child 1").Add("child 2").Add("child 4")

callback := func(wn *gtree.WalkerNode) error {
fmt.Println(wn.Row())
return nil
}

if err := gtree.WalkProgrammably(root, callback); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Output:
// root
// ├── child 1
// │ └── child 2
// │ ├── child 3
// │ └── child 4
// └── child 5

callback2 := func(wn *gtree.WalkerNode) error {
fmt.Println("WalkerNode's methods called...")
fmt.Printf("\tName : %s\n", wn.Name())
fmt.Printf("\tBranch : %s\n", wn.Branch())
fmt.Printf("\tRow : %s\n", wn.Row())
fmt.Printf("\tLevel : %d\n", wn.Level())
fmt.Printf("\tPath : %s\n", wn.Path())
fmt.Printf("\tHasChild : %t\n", wn.HasChild())
return nil
}

if err := gtree.WalkProgrammably(root, callback2); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Output:
// WalkerNode's methods called...
// Name : root
// Branch :
// Row : root
// Level : 1
// Path : root
// HasChild : true
// WalkerNode's methods called...
// Name : child 1
// Branch : ├──
// Row : ├── child 1
// Level : 2
// Path : root/child 1
// HasChild : true
// WalkerNode's methods called...
// Name : child 2
// Branch : │ └──
// Row : │ └── child 2
// Level : 3
// Path : root/child 1/child 2
// HasChild : true
// WalkerNode's methods called...
// Name : child 3
// Branch : │ ├──
// Row : │ ├── child 3
// Level : 4
// Path : root/child 1/child 2/child 3
// HasChild : false
// WalkerNode's methods called...
// Name : child 4
// Branch : │ └──
// Row : │ └── child 4
// Level : 4
// Path : root/child 1/child 2/child 4
// HasChild : false
// WalkerNode's methods called...
// Name : child 5
// Branch : └──
// Row : └── child 5
// Level : 2
// Path : root/child 5
// HasChild : false
}

In this way, the user can execute a function defined by the user for each node that makes up the tree.
From the root node (the node created by the gtree.NewRoot function), each child node is recursively traversed and the defined function is processed.

To try it out, let’s change the mere utility program introduced in the first section.

package main

import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/ddddddO/gtree"
)

// Example:
// $ cd github.com/ddddddO/gtree
// $ find . -type d -name .git -prune -o -type f -print
// ./config.go
// ./node_generator_test.go
// ./example/like_cli/adapter/indentation.go
// ./example/like_cli/adapter/executor.go
// ./example/like_cli/main.go
// ./example/find_pipe_programmable-gtree/main.go
// ...
// $ find . -type d -name .git -prune -o -type f -print | go run example/find_pipe_programmable-gtree/main.go
// << See "Output:" below. >>
func main() {
var (
root *gtree.Node
node *gtree.Node
)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text() // e.g.) "./example/find_pipe_programmable-gtree/main.go"
splited := strings.Split(line, "/") // e.g.) [. example find_pipe_programmable-gtree main.go]

for i, s := range splited {
if root == nil {
root = gtree.NewRoot(s) // s := "."
node = root
continue
}
if i == 0 {
continue
}

tmp := node.Add(s)
node = tmp
}
node = root
}

base, err := os.Getwd()
if err != nil {
os.Exit(1)
}
callback := func(wn *gtree.WalkerNode) error {
path := filepath.Join(base, wn.Path())
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

finfo, err := file.Stat()
if err != nil {
return err
}

fmt.Printf("[%07d] : %-70s : %s\n", finfo.Size(), wn.Row(), wn.Path())
return nil
}

if err := gtree.WalkProgrammably(root, callback); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Output:
// [0004096] : . : .
// [0002664] : ├── config.go : config.go
// [0005133] : ├── node_generator_test.go : node_generator_test.go
// [0004096] : ├── example : example
// [0001687] : │ ├── README.md : example/README.md
// [0004096] : │ ├── noexist : example/noexist
// [0000000] : │ │ └── xxx : example/noexist/xxx
// [0004096] : │ ├── like_cli : example/like_cli
// [0004096] : │ │ ├── adapter : example/like_cli/adapter
// [0000770] : │ │ │ ├── indentation.go : example/like_cli/adapter/indentation.go
// [0000080] : │ │ │ └── executor.go : example/like_cli/adapter/executor.go
// [0003652] : │ │ └── main.go : example/like_cli/main.go
// [0004096] : │ ├── find_pipe_programmable-gtree : example/find_pipe_programmable-gtree
// [0000796] : │ │ ├── README.md : example/find_pipe_programmable-gtree/README.md
// [0124908] : │ │ ├── main.go : example/find_pipe_programmable-gtree/main.go
// [0000420] : │ │ ├── go.mod : example/find_pipe_programmable-gtree/go.mod
// [0003039] : │ │ └── go.sum : example/find_pipe_programmable-gtree/go.sum
// [0004096] : │ ├── go-list_pipe_programmable-gtree : example/go-list_pipe_programmable-gtree
// [0000618] : │ │ ├── README.md : example/go-list_pipe_programmable-gtree/README.md
// [0003840] : │ │ ├── main.go : example/go-list_pipe_programmable-gtree/main.go
// [0000423] : │ │ ├── go.mod : example/go-list_pipe_programmable-gtree/go.mod
// [0003039] : │ │ └── go.sum : example/go-list_pipe_programmable-gtree/go.sum
// [0004096] : │ └── programmable : example/programmable
// [0014286] : │ └── main.go : example/programmable/main.go
// [0000424] : ├── file_considerer.go : file_considerer.go
// [0001972] : ├── node.go : node.go
// [0002891] : ├── simple_tree_grower.go : simple_tree_grower.go
// [0000977] : ├── node_generator.go : node_generator.go
// ...
}

It worked! It’s hard to understand the output result, so I’ll paste an image.

  • Size of each file
  • tree view
  • each file path

The above three outputs are now available.

Since node information can be taken from the methods of the `WalkerNode` structure, it can be used.

--

--