🌲

Goでtreeを表現する

2021/08/05に公開

どういうこと?

Linuxのtreeコマンド結果のような出力をGoのソースコードで実現できるパッケージを作りました(その宣伝です)。
この記事を公開する前に「Markdown形式の入力からtreeを出力するCLI/Web」という記事を書きました。こちらの記事で紹介しているCLIで利用しているコードを流用しています。

デモ

まずは見て頂くのが早いと思うので以下にサンプルを載せます。

package main

import (
	"fmt"
	"os"

	"github.com/ddddddO/gtree"
)

func main() {
	var root *gtree.Node
	root = gtree.NewRoot("root")
	root.Add("child 1").Add("child 2")
	root.Add("child 1").Add("child 3")
	child4 := root.Add("child 4")

	var child7 *gtree.Node
	child7 = child4.Add("child 5").Add("child 6").Add("child 7")
	child7.Add("child 8")

	if err := gtree.OutputProgrammably(os.Stdout, root); err != nil {
		fmt.Println(err)
		return
	}
}

上記の実行結果が以下です。

root
├── child 1
│   ├── child 2
│   └── child 3
└── child 4
    └── child 5
        └── child 6
            └── child 7
                └── child 8

このように、ツリーを構成するノード群をGoのコードで生成し、出力することができます。

どんなときに使えるの?

...今のところ思いついていません。ただ、使う人によっていろいろな事物をツリーで表現できるのでは?と思いました。
例えば、findコマンドの結果を標準入力に渡しGoコードを実行するとLinux tree結果のような出力をするプログラムを以下に載せます。

package main

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

	"github.com/ddddddO/gtree"
)

// $ 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
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
	// ...
}

任意のコマンドをパイプして、上記のようなGoプログラムに実行させれば夢は広がるのでは?と思いました。

追記

上記は、コード上でツリーを定義し、 gtree.OutputProgrammably 関数でツリーを出力するというものでした。
もし、ツリーになんらか手を加えたい場合(外部のライブラリを利用するなど)は、 gtree.WalkProgrammably 関数を使うと良いかもしれません。以下がサンプルのコードです。

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
}

このように、ツリーを構成する各ノード毎に利用者自身で定義した関数を実行できる仕組みです。
Rootノード(gtree.NewRoot関数で作成されるノード)から各子ノードを再帰的に走査し、定義された関数が処理されます。

試しに、「どんなときに使えるの?」セクションで紹介したプログラムを変更してみます。

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
	// ...

実行結果が見づらいので、以下スクショです。

  • 各ファイルのサイズ
  • ツリー表示
  • 各ファイルパス

を出力するようにしました。
WalkerNode 構造体のメソッドからノードの情報が取れるため、それが利用できます。

その他

インストール

Goは1.18以上が必要です。

go get github.com/ddddddO/gtree

リポジトリ

https://github.com/ddddddO/gtree

ドキュメント

https://pkg.go.dev/github.com/ddddddO/gtree

感想

途中で、書いてるコードが複雑になり始めてきて諦めようと思いました。しかし、少し間をおいて考えた結果、シンプルな実装にすることが出来たので、頭を冷やす時間は大事と思いました。
また、ちょっとしたリファクタリング(変数・メソッド名を説明的にする/役割り毎にファイルを分けるなど)を度々繰り返すことで、最初はCLIとして作っていましたが、パッケージとして提供する際はかなりスムーズにできました。リファクタリング大事。
あとは、GoDocの作り方とか、Goはv2からgo.modを編集しないといけないなどの学びがありました。

更新情報

  • 2022/08/17 Wasm 対応をしてGitHub Pagesにホストしました🎉
  • 2022/01/15 ディレクトリとファイルを作成できる関数を作りました🎉
  • 2021/11/20 出力にJSON/YAML/TOMLを対応しました🎉
  • パッケージ限定で、ユーザー自身が枝のフォーマットを決められる機能を追加しました🎉
    • こちらにサンプルを載せています🙇

Discussion