Goでtreeを表現する
どういうこと?
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
リポジトリ
ドキュメント
感想
途中で、書いてるコードが複雑になり始めてきて諦めようと思いました。しかし、少し間をおいて考えた結果、シンプルな実装にすることが出来たので、頭を冷やす時間は大事と思いました。
また、ちょっとしたリファクタリング(変数・メソッド名を説明的にする/役割り毎にファイルを分けるなど)を度々繰り返すことで、最初はCLIとして作っていましたが、パッケージとして提供する際はかなりスムーズにできました。リファクタリング大事。
あとは、GoDocの作り方とか、Goはv2からgo.modを編集しないといけないなどの学びがありました。
更新情報
- 2022/08/17 Wasm 対応をしてGitHub Pagesにホストしました🎉
- 2022/01/15 ディレクトリとファイルを作成できる関数を作りました🎉
- 2021/11/20 出力にJSON/YAML/TOMLを対応しました🎉
- パッケージ限定で、ユーザー自身が枝のフォーマットを決められる機能を追加しました🎉
- こちらにサンプルを載せています🙇
Discussion