diff --git a/.gitignore b/.gitignore
index 057dc5f..da74d5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ root4/
root5/
root6/
root7/
+root8/
root_a/
root_b/
root_c/
@@ -20,4 +21,5 @@ root_f/
root_g/
root_h/
root_i/
+root_j/
Primate/
diff --git a/Makefile b/Makefile
index f5fc5bd..4a1c1f8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
sweep:
- rm -rf ./root/ ./root1/ ./root2/ ./root3/ ./root4/ ./root5/ ./root6/ ./root7/ Primate/ gtree/
- rm -rf ./root_a/ ./root_b/ ./root_c/ ./root_d/ ./root_e/ ./root_f/ ./root_g/ ./root_h/ ./root_i/
+ rm -rf ./root/ ./root1/ ./root2/ ./root3/ ./root4/ ./root5/ ./root6/ ./root7/ ./root8/ Primate/ gtree/
+ rm -rf ./root_a/ ./root_b/ ./root_c/ ./root_d/ ./root_e/ ./root_f/ ./root_g/ ./root_h/ ./root_i/ ./root_j/
fmt: sweep
go fmt ./...
diff --git a/README.md b/README.md
index e379c3c..62b50ff 100644
--- a/README.md
+++ b/README.md
@@ -158,6 +158,7 @@ OPTIONS:
--file value, -f value specify the path to markdown file. (default: stdin)
--two-spaces, --ts set this option when the markdown indent is 2 spaces. (default: tab spaces)
--four-spaces, --fs set this option when the markdown indent is 4 spaces. (default: tab spaces)
+ --massive, -m set this option when there are very many blocks of markdown. (default: false)
--json, -j set this option when outputting JSON. (default: tree)
--yaml, -y set this option when outputting YAML. (default: tree)
--toml, -t set this option when outputting TOML. (default: tree)
@@ -400,6 +401,7 @@ OPTIONS:
--file value, -f value specify the path to markdown file. (default: stdin)
--two-spaces, --ts set this option when the markdown indent is 2 spaces. (default: tab spaces)
--four-spaces, --fs set this option when the markdown indent is 4 spaces. (default: tab spaces)
+ --massive, -m set this option when there are very many blocks of markdown. (default: false)
--dry-run, -d, --dr dry run. detects node that is invalid for directory generation.
the order of the output and made directories does not always match. (default: false)
--extension value, -e value, --ext value [ --extension value, -e value, --ext value ] set this option if you want to create file instead of directory.
@@ -951,27 +953,31 @@ func main() {
# Process
+> **Note**
+> This process is for the Massive Roots mode.
-## e.g. [*gtree/tree_handler.go*](https://github.com/ddddddO/gtree/blob/master/tree_handler.go)
+## e.g. [*gtree/pipeline_tree.go*](https://github.com/ddddddO/gtree/blob/master/pipeline_tree.go)
# Performance
-
> **Warning**
> Depends on the environment.
-- Comparison before and after software architecture was changed.
-- In the case of few Roots, previous architecture is faster in execution😅
-- However, for multiple Roots, execution speed tends to be faster💪✨
+- Comparison simple implementation and pipeline implementation.
+- In the case of few Roots, simple implementation is faster in execution!
+ - Use this one by default.
+- However, for multiple Roots, pipeline implementation execution speed tends to be faster💪✨
+ - In the CLI, it is available by specifying `--massive`.
+ - In the Go program, it is available by specifying `WithMassive` func.
Benchmark log
-## Before pipelining
+## Simple implementation
```console
$ go test -benchmem -bench Benchmark -benchtime 100x tree_handler_benchmark_test.go
goos: linux
@@ -991,7 +997,7 @@ PASS
ok command-line-arguments 68.124s
```
-## After pipelining
+## Pipeline implementation
```console
$ go test -benchmem -bench Benchmark -benchtime 100x tree_handler_benchmark_test.go
goos: linux
diff --git a/cmd/gtree/main.go b/cmd/gtree/main.go
index a651037..b44b0aa 100644
--- a/cmd/gtree/main.go
+++ b/cmd/gtree/main.go
@@ -37,6 +37,11 @@ func main() {
Usage: "set this option when the markdown indent is 4 spaces.",
DefaultText: "tab spaces",
},
+ &cli.BoolFlag{
+ Name: "massive",
+ Aliases: []string{"m"},
+ Usage: "set this option when there are very many blocks of markdown.",
+ },
}
outputFlags := []cli.Flag{
diff --git a/config.go b/config.go
index 4ab6b82..b53d628 100644
--- a/config.go
+++ b/config.go
@@ -5,6 +5,7 @@ type config struct {
intermedialNodeFormat branchFormat
space spaceType
+ massive bool
encode encode
dryrun bool
fileExtensions []string
@@ -20,8 +21,9 @@ func newConfig(options []Option) (*config, error) {
directly: "├──",
indirectly: "│ ",
},
- space: spacesTab,
- encode: encodeDefault,
+ space: spacesTab,
+ massive: false,
+ encode: encodeDefault,
}
for _, opt := range options {
if opt == nil {
@@ -72,6 +74,14 @@ func WithBranchFormatLastNode(directly, indirectly string) Option {
}
}
+// WithMassive returns function for large amount roots.
+func WithMassive() Option {
+ return func(c *config) error {
+ c.massive = true
+ return nil
+ }
+}
+
// WithEncodeJSON returns function for output json format.
func WithEncodeJSON() Option {
return func(c *config) error {
diff --git a/pipeline_tree.go b/pipeline_tree.go
new file mode 100644
index 0000000..b888d23
--- /dev/null
+++ b/pipeline_tree.go
@@ -0,0 +1,158 @@
+//go:build !wasm
+
+package gtree
+
+import (
+ "context"
+ "io"
+
+ "github.com/fatih/color"
+ "golang.org/x/sync/errgroup"
+)
+
+type treePipeline struct {
+ grower growerPipeline
+ spreader spreaderPipeline
+ mkdirer mkdirerPipeline
+}
+
+func newTreePipeline(conf *config) *treePipeline {
+ growerFactory := func(lastNodeFormat, intermedialNodeFormat branchFormat, dryrun bool, encode encode) growerPipeline {
+ if encode != encodeDefault {
+ return newNopGrowerPipeline()
+ }
+ return newGrowerPipeline(lastNodeFormat, intermedialNodeFormat, dryrun)
+ }
+
+ spreaderFactory := func(encode encode, dryrun bool, fileExtensions []string) spreaderPipeline {
+ if dryrun {
+ return newColorizeSpreaderPipeline(fileExtensions)
+ }
+ return newSpreaderPipeline(encode)
+ }
+
+ mkdirerFactory := func(fileExtensions []string) mkdirerPipeline {
+ return newMkdirerPipeline(fileExtensions)
+ }
+
+ return &treePipeline{
+ grower: growerFactory(
+ conf.lastNodeFormat,
+ conf.intermedialNodeFormat,
+ conf.dryrun,
+ conf.encode,
+ ),
+ spreader: spreaderFactory(
+ conf.encode,
+ conf.dryrun,
+ conf.fileExtensions,
+ ),
+ mkdirer: mkdirerFactory(
+ conf.fileExtensions,
+ ),
+ }
+}
+
+func (t *treePipeline) output(w io.Writer, r io.Reader, conf *config) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ splitStream, errcsl := split(ctx, r)
+ rootStream, errcr := newRootGeneratorPipeline(conf.space).generate(ctx, splitStream)
+ growStream, errcg := t.grow(ctx, rootStream)
+ errcs := t.spread(ctx, w, growStream)
+ return t.handlePipelineErr(errcsl, errcr, errcg, errcs)
+}
+
+func (t *treePipeline) outputProgrammably(w io.Writer, root *Node, conf *config) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ rootStream := make(chan *Node)
+ go func() {
+ defer close(rootStream)
+ rootStream <- root
+ }()
+ growStream, errcg := t.grow(ctx, rootStream)
+ errcs := t.spread(ctx, w, growStream)
+ return t.handlePipelineErr(errcg, errcs)
+}
+
+func (t *treePipeline) makedir(r io.Reader, conf *config) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ splitStream, errcsl := split(ctx, r)
+ rootStream, errcr := newRootGeneratorPipeline(conf.space).generate(ctx, splitStream)
+ growStream, errcg := t.grow(ctx, rootStream)
+ errcm := t.mkdir(ctx, growStream)
+ return t.handlePipelineErr(errcsl, errcr, errcg, errcm)
+}
+
+func (t *treePipeline) makedirProgrammably(root *Node, conf *config) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ rootStream := make(chan *Node)
+ go func() {
+ defer close(rootStream)
+ rootStream <- root
+ }()
+ t.enableValidation()
+ // when detect invalid node name, return error. process end.
+ growStream, errcg := t.grow(ctx, rootStream)
+ if conf.dryrun {
+ // when detected no invalid node name, output tree.
+ errcs := t.spread(ctx, color.Output, growStream)
+ return t.handlePipelineErr(errcg, errcs)
+ }
+ // when detected no invalid node name, no output tree.
+ errcm := t.mkdir(ctx, growStream)
+ return t.handlePipelineErr(errcg, errcm)
+}
+
+// 関心事は各ノードの枝の形成
+type growerPipeline interface {
+ grow(context.Context, <-chan *Node) (<-chan *Node, <-chan error)
+ enableValidation()
+}
+
+// 関心事はtreeの出力
+type spreaderPipeline interface {
+ spread(context.Context, io.Writer, <-chan *Node) <-chan error
+}
+
+// 関心事はファイルの生成
+// interfaceを使う必要はないが、growerPipeline/spreaderPipelineと合わせたいため
+type mkdirerPipeline interface {
+ mkdir(context.Context, <-chan *Node) <-chan error
+}
+
+func (t *treePipeline) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) {
+ return t.grower.grow(ctx, roots)
+}
+
+func (t *treePipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
+ return t.spreader.spread(ctx, w, roots)
+}
+
+func (t *treePipeline) mkdir(ctx context.Context, roots <-chan *Node) <-chan error {
+ return t.mkdirer.mkdir(ctx, roots)
+}
+
+// パイプラインの全ステージで最初のエラーを返却
+func (*treePipeline) handlePipelineErr(echs ...<-chan error) error {
+ eg, _ := errgroup.WithContext(context.TODO())
+ for i := range echs {
+ i := i
+ eg.Go(func() error {
+ for e := range echs[i] {
+ if e != nil {
+ return e
+ }
+ }
+ return nil
+ })
+ }
+ return eg.Wait()
+}
diff --git a/tree_grower.go b/pipeline_tree_grower.go
similarity index 70%
rename from tree_grower.go
rename to pipeline_tree_grower.go
index 7f7efac..930308f 100644
--- a/tree_grower.go
+++ b/pipeline_tree_grower.go
@@ -7,26 +7,22 @@ import (
"sync"
)
-func newGrower(
+func newGrowerPipeline(
lastNodeFormat, intermedialNodeFormat branchFormat,
enabledValidation bool,
-) grower {
- return &defaultGrower{
+) growerPipeline {
+ return &defaultGrowerPipeline{
lastNodeFormat: lastNodeFormat,
intermedialNodeFormat: intermedialNodeFormat,
enabledValidation: enabledValidation,
}
}
-func newNopGrower() grower {
- return &nopGrower{}
+func newNopGrowerPipeline() growerPipeline {
+ return &nopGrowerPipeline{}
}
-type branchFormat struct {
- directly, indirectly string
-}
-
-type defaultGrower struct {
+type defaultGrowerPipeline struct {
lastNodeFormat branchFormat
intermedialNodeFormat branchFormat
enabledValidation bool
@@ -34,7 +30,7 @@ type defaultGrower struct {
const workerGrowNum = 10
-func (dg *defaultGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) {
+func (dg *defaultGrowerPipeline) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) {
nodes := make(chan *Node)
errc := make(chan error, 1)
@@ -55,7 +51,7 @@ func (dg *defaultGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *
return nodes, errc
}
-func (dg *defaultGrower) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, nodes chan<- *Node, errc chan<- error) {
+func (dg *defaultGrowerPipeline) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, nodes chan<- *Node, errc chan<- error) {
defer wg.Done()
for {
select {
@@ -74,7 +70,7 @@ func (dg *defaultGrower) worker(ctx context.Context, wg *sync.WaitGroup, roots <
}
}
-func (dg *defaultGrower) assemble(current *Node) error {
+func (dg *defaultGrowerPipeline) assemble(current *Node) error {
if err := dg.assembleBranch(current); err != nil {
return err
}
@@ -87,7 +83,7 @@ func (dg *defaultGrower) assemble(current *Node) error {
return nil
}
-func (dg *defaultGrower) assembleBranch(current *Node) error {
+func (dg *defaultGrowerPipeline) assembleBranch(current *Node) error {
current.clean() // 例えば、MkdirProgrammably funcでrootノードを使いまわすと、前回func実行時に形成されたノードの枝が残ったまま追記されてしまうため。
dg.assembleBranchDirectly(current)
@@ -108,7 +104,7 @@ func (dg *defaultGrower) assembleBranch(current *Node) error {
return nil
}
-func (dg *defaultGrower) assembleBranchDirectly(current *Node) {
+func (dg *defaultGrowerPipeline) assembleBranchDirectly(current *Node) {
if current == nil || current.isRoot() {
return
}
@@ -122,7 +118,7 @@ func (dg *defaultGrower) assembleBranchDirectly(current *Node) {
}
}
-func (dg *defaultGrower) assembleBranchIndirectly(current, parent *Node) {
+func (dg *defaultGrowerPipeline) assembleBranchIndirectly(current, parent *Node) {
if current == nil || parent == nil || current.isRoot() {
return
}
@@ -136,7 +132,7 @@ func (dg *defaultGrower) assembleBranchIndirectly(current, parent *Node) {
}
}
-func (*defaultGrower) assembleBranchFinally(current, root *Node) {
+func (*defaultGrowerPipeline) assembleBranchFinally(current, root *Node) {
if current == nil {
return
}
@@ -146,13 +142,13 @@ func (*defaultGrower) assembleBranchFinally(current, root *Node) {
}
}
-func (dg *defaultGrower) enableValidation() {
+func (dg *defaultGrowerPipeline) enableValidation() {
dg.enabledValidation = true
}
-type nopGrower struct{}
+type nopGrowerPipeline struct{}
-func (*nopGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) {
+func (*nopGrowerPipeline) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) {
nodes := make(chan *Node)
errc := make(chan error, 1)
@@ -179,9 +175,9 @@ func (*nopGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <
return nodes, errc
}
-func (*nopGrower) enableValidation() {}
+func (*nopGrowerPipeline) enableValidation() {}
var (
- _ grower = (*defaultGrower)(nil)
- _ grower = (*nopGrower)(nil)
+ _ growerPipeline = (*defaultGrowerPipeline)(nil)
+ _ growerPipeline = (*nopGrowerPipeline)(nil)
)
diff --git a/tree_mkdirer.go b/pipeline_tree_mkdirer.go
similarity index 66%
rename from tree_mkdirer.go
rename to pipeline_tree_mkdirer.go
index 6108a8d..fc26c16 100644
--- a/tree_mkdirer.go
+++ b/pipeline_tree_mkdirer.go
@@ -9,19 +9,19 @@ import (
"sync"
)
-func newMkdirer(fileExtensions []string) mkdirer {
- return &defaultMkdirer{
+func newMkdirerPipeline(fileExtensions []string) mkdirerPipeline {
+ return &defaultMkdirerPipeline{
fileConsiderer: newFileConsiderer(fileExtensions),
}
}
-type defaultMkdirer struct {
+type defaultMkdirerPipeline struct {
fileConsiderer *fileConsiderer
}
const workerMkdirNum = 10
-func (dm *defaultMkdirer) mkdir(ctx context.Context, roots <-chan *Node) <-chan error {
+func (dm *defaultMkdirerPipeline) mkdir(ctx context.Context, roots <-chan *Node) <-chan error {
errc := make(chan error, 1)
go func() {
@@ -38,7 +38,7 @@ func (dm *defaultMkdirer) mkdir(ctx context.Context, roots <-chan *Node) <-chan
return errc
}
-func (dm *defaultMkdirer) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, errc chan<- error) {
+func (dm *defaultMkdirerPipeline) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, errc chan<- error) {
defer wg.Done()
for {
select {
@@ -60,14 +60,14 @@ func (dm *defaultMkdirer) worker(ctx context.Context, wg *sync.WaitGroup, roots
}
}
-func (*defaultMkdirer) isExistRoot(root *Node) bool {
+func (*defaultMkdirerPipeline) isExistRoot(root *Node) bool {
if _, err := os.Stat(root.path()); !os.IsNotExist(err) {
return true
}
return false
}
-func (dm *defaultMkdirer) makeDirectoriesAndFiles(current *Node) error {
+func (dm *defaultMkdirerPipeline) makeDirectoriesAndFiles(current *Node) error {
if dm.fileConsiderer.isFile(current) {
dir := strings.TrimSuffix(current.path(), current.name)
if err := dm.mkdirAll(dir); err != nil {
@@ -90,11 +90,11 @@ func (dm *defaultMkdirer) makeDirectoriesAndFiles(current *Node) error {
const permission = 0o755
-func (*defaultMkdirer) mkdirAll(dir string) error {
+func (*defaultMkdirerPipeline) mkdirAll(dir string) error {
return os.MkdirAll(dir, permission)
}
-func (*defaultMkdirer) mkfile(path string) error {
+func (*defaultMkdirerPipeline) mkfile(path string) error {
f, err := os.Create(path)
if err != nil {
return err
@@ -102,4 +102,4 @@ func (*defaultMkdirer) mkfile(path string) error {
return f.Close()
}
-var _ mkdirer = (*defaultMkdirer)(nil)
+var _ mkdirerPipeline = (*defaultMkdirerPipeline)(nil)
diff --git a/tree_spreader.go b/pipeline_tree_spreader.go
similarity index 52%
rename from tree_spreader.go
rename to pipeline_tree_spreader.go
index b549639..df7db94 100644
--- a/tree_spreader.go
+++ b/pipeline_tree_spreader.go
@@ -15,21 +15,21 @@ import (
"gopkg.in/yaml.v3"
)
-func newSpreader(encode encode) spreader {
+func newSpreaderPipeline(encode encode) spreaderPipeline {
switch encode {
case encodeJSON:
- return &jsonSpreader{}
+ return &jsonSpreaderPipeline{}
case encodeYAML:
- return &yamlSpreader{}
+ return &yamlSpreaderPipeline{}
case encodeTOML:
- return &tomlSpreader{}
+ return &tomlSpreaderPipeline{}
default:
- return &defaultSpreader{}
+ return &defaultSpreaderPipeline{}
}
}
-func newColorizeSpreader(fileExtensions []string) spreader {
- return &colorizeSpreader{
+func newColorizeSpreaderPipeline(fileExtensions []string) spreaderPipeline {
+ return &colorizeSpreaderPipeline{
fileConsiderer: newFileConsiderer(fileExtensions),
fileColor: color.New(color.Bold, color.FgHiCyan),
fileCounter: newCounter(),
@@ -39,22 +39,13 @@ func newColorizeSpreader(fileExtensions []string) spreader {
}
}
-type encode int
-
-const (
- encodeDefault encode = iota
- encodeJSON
- encodeYAML
- encodeTOML
-)
-
-type defaultSpreader struct {
+type defaultSpreaderPipeline struct {
sync.Mutex
}
const workerSpreadNum = 10
-func (ds *defaultSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
+func (ds *defaultSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
errc := make(chan error, 1)
go func() {
@@ -76,7 +67,7 @@ func (ds *defaultSpreader) spread(ctx context.Context, w io.Writer, roots <-chan
return errc
}
-func (ds *defaultSpreader) worker(ctx context.Context, wg *sync.WaitGroup, bw *bufio.Writer, roots <-chan *Node, errc chan<- error) {
+func (ds *defaultSpreaderPipeline) worker(ctx context.Context, wg *sync.WaitGroup, bw *bufio.Writer, roots <-chan *Node, errc chan<- error) {
defer wg.Done()
for {
select {
@@ -97,19 +88,19 @@ func (ds *defaultSpreader) worker(ctx context.Context, wg *sync.WaitGroup, bw *b
}
}
-func (*defaultSpreader) spreadBranch(current *Node) string {
+func (*defaultSpreaderPipeline) spreadBranch(current *Node) string {
ret := current.name + "\n"
if !current.isRoot() {
ret = current.branch() + " " + current.name + "\n"
}
for _, child := range current.children {
- ret += (*defaultSpreader)(nil).spreadBranch(child)
+ ret += (*defaultSpreaderPipeline)(nil).spreadBranch(child)
}
return ret
}
-type colorizeSpreader struct {
+type colorizeSpreaderPipeline struct {
fileConsiderer *fileConsiderer
fileColor *color.Color
fileCounter *counter
@@ -118,7 +109,7 @@ type colorizeSpreader struct {
dirCounter *counter
}
-func (cs *colorizeSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
+func (cs *colorizeSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
errc := make(chan error, 1)
go func() {
@@ -157,7 +148,7 @@ func (cs *colorizeSpreader) spread(ctx context.Context, w io.Writer, roots <-cha
return errc
}
-func (cs *colorizeSpreader) spreadBranch(current *Node) string {
+func (cs *colorizeSpreaderPipeline) spreadBranch(current *Node) string {
ret := ""
if current.isRoot() {
ret = cs.colorize(current) + "\n"
@@ -171,7 +162,7 @@ func (cs *colorizeSpreader) spreadBranch(current *Node) string {
return ret
}
-func (cs *colorizeSpreader) colorize(current *Node) string {
+func (cs *colorizeSpreaderPipeline) colorize(current *Node) string {
if cs.fileConsiderer.isFile(current) {
_ = cs.fileCounter.next()
return cs.fileColor.Sprint(current.name)
@@ -181,7 +172,7 @@ func (cs *colorizeSpreader) colorize(current *Node) string {
}
}
-func (cs *colorizeSpreader) summary() string {
+func (cs *colorizeSpreaderPipeline) summary() string {
return fmt.Sprintf(
"%d directories, %d files",
cs.dirCounter.current(),
@@ -189,9 +180,9 @@ func (cs *colorizeSpreader) summary() string {
)
}
-type jsonSpreader struct{}
+type jsonSpreaderPipeline struct{}
-func (*jsonSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
+func (*jsonSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
errc := make(chan error, 1)
go func() {
@@ -218,31 +209,9 @@ func (*jsonSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node
return errc
}
-type jsonNode struct {
- Name string `json:"value"`
- Children []*jsonNode `json:"children"`
-}
-
-func (parent *Node) toJSONNode(jParent *jsonNode) *jsonNode {
- if jParent == nil {
- jParent = &jsonNode{Name: parent.name}
- }
- if !parent.hasChild() {
- return jParent
- }
+type tomlSpreaderPipeline struct{}
- jParent.Children = make([]*jsonNode, len(parent.children))
- for i := range parent.children {
- jParent.Children[i] = &jsonNode{Name: parent.children[i].name}
- _ = parent.children[i].toJSONNode(jParent.Children[i])
- }
-
- return jParent
-}
-
-type tomlSpreader struct{}
-
-func (*tomlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
+func (*tomlSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
errc := make(chan error, 1)
go func() {
@@ -269,31 +238,9 @@ func (*tomlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node
return errc
}
-type tomlNode struct {
- Name string `toml:"value"`
- Children []*tomlNode `toml:"children"`
-}
-
-func (parent *Node) toTOMLNode(tParent *tomlNode) *tomlNode {
- if tParent == nil {
- tParent = &tomlNode{Name: parent.name}
- }
- if !parent.hasChild() {
- return tParent
- }
-
- tParent.Children = make([]*tomlNode, len(parent.children))
- for i := range parent.children {
- tParent.Children[i] = &tomlNode{Name: parent.children[i].name}
- _ = parent.children[i].toTOMLNode(tParent.Children[i])
- }
-
- return tParent
-}
-
-type yamlSpreader struct{}
+type yamlSpreaderPipeline struct{}
-func (*yamlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
+func (*yamlSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
errc := make(chan error, 1)
go func() {
@@ -320,32 +267,10 @@ func (*yamlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node
return errc
}
-type yamlNode struct {
- Name string `yaml:"value"`
- Children []*yamlNode `yaml:"children"`
-}
-
-func (parent *Node) toYAMLNode(yParent *yamlNode) *yamlNode {
- if yParent == nil {
- yParent = &yamlNode{Name: parent.name}
- }
- if !parent.hasChild() {
- return yParent
- }
-
- yParent.Children = make([]*yamlNode, len(parent.children))
- for i := range parent.children {
- yParent.Children[i] = &yamlNode{Name: parent.children[i].name}
- _ = parent.children[i].toYAMLNode(yParent.Children[i])
- }
-
- return yParent
-}
-
var (
- _ spreader = (*defaultSpreader)(nil)
- _ spreader = (*colorizeSpreader)(nil)
- _ spreader = (*jsonSpreader)(nil)
- _ spreader = (*yamlSpreader)(nil)
- _ spreader = (*tomlSpreader)(nil)
+ _ spreaderPipeline = (*defaultSpreaderPipeline)(nil)
+ _ spreaderPipeline = (*colorizeSpreaderPipeline)(nil)
+ _ spreaderPipeline = (*jsonSpreaderPipeline)(nil)
+ _ spreaderPipeline = (*yamlSpreaderPipeline)(nil)
+ _ spreaderPipeline = (*tomlSpreaderPipeline)(nil)
)
diff --git a/root_generator.go b/root_generator.go
index 3541e3a..5ea2e25 100644
--- a/root_generator.go
+++ b/root_generator.go
@@ -5,23 +5,71 @@ package gtree
import (
"bufio"
"context"
+ "io"
"strings"
"sync"
)
-type rootGenerator struct {
+type rootGeneratorSimple struct {
+ counter *counter
+ scanner *bufio.Scanner
nodeGenerator *nodeGenerator
}
-func newRootGenerator(st spaceType) *rootGenerator {
- return &rootGenerator{
+func newRootGeneratorSimple(r io.Reader, st spaceType) *rootGeneratorSimple {
+ return &rootGeneratorSimple{
+ counter: newCounter(),
+ scanner: bufio.NewScanner(r),
+ nodeGenerator: newNodeGenerator(st),
+ }
+}
+
+func (rg *rootGeneratorSimple) generate() ([]*Node, error) {
+ var (
+ stack *stack
+ roots []*Node
+ )
+
+ for rg.scanner.Scan() {
+ currentNode, err := rg.nodeGenerator.generate(rg.scanner.Text(), rg.counter.next())
+ if err != nil {
+ return nil, err
+ }
+ if currentNode == nil {
+ continue
+ }
+
+ if currentNode.isRoot() {
+ rg.counter.reset()
+ roots = append(roots, currentNode)
+ stack = newStack()
+ stack.push(currentNode)
+ continue
+ }
+
+ if stack == nil {
+ return nil, errNilStack
+ }
+
+ stack.dfs(currentNode)
+ }
+
+ return roots, rg.scanner.Err()
+}
+
+type rootGeneratorPipeline struct {
+ nodeGenerator *nodeGenerator
+}
+
+func newRootGeneratorPipeline(st spaceType) *rootGeneratorPipeline {
+ return &rootGeneratorPipeline{
nodeGenerator: newNodeGenerator(st),
}
}
const workerGenerateNum = 10
-func (rg *rootGenerator) generate(ctx context.Context, blocks <-chan string) (<-chan *Node, <-chan error) {
+func (rg *rootGeneratorPipeline) generate(ctx context.Context, blocks <-chan string) (<-chan *Node, <-chan error) {
rootc := make(chan *Node)
errc := make(chan error, 1)
@@ -42,7 +90,7 @@ func (rg *rootGenerator) generate(ctx context.Context, blocks <-chan string) (<-
return rootc, errc
}
-func (rg *rootGenerator) worker(ctx context.Context, wg *sync.WaitGroup, blocks <-chan string, rootc chan<- *Node, errc chan<- error) {
+func (rg *rootGeneratorPipeline) worker(ctx context.Context, wg *sync.WaitGroup, blocks <-chan string, rootc chan<- *Node, errc chan<- error) {
defer wg.Done()
for {
select {
diff --git a/simple_tree.go b/simple_tree.go
new file mode 100644
index 0000000..e8b5bf6
--- /dev/null
+++ b/simple_tree.go
@@ -0,0 +1,128 @@
+//go:build !wasm
+
+package gtree
+
+import (
+ "io"
+
+ "github.com/fatih/color"
+)
+
+type treeSimple struct {
+ grower growerSimple
+ spreader spreaderSimple
+ mkdirer mkdirerSimple
+}
+
+func newTreeSimple(conf *config) *treeSimple {
+ growerFactory := func(lastNodeFormat, intermedialNodeFormat branchFormat, dryrun bool, encode encode) growerSimple {
+ if encode != encodeDefault {
+ return newNopGrowerSimple()
+ }
+ return newGrowerSimple(lastNodeFormat, intermedialNodeFormat, dryrun)
+ }
+
+ spreaderFactory := func(encode encode, dryrun bool, fileExtensions []string) spreaderSimple {
+ if dryrun {
+ return newColorizeSpreaderSimple(fileExtensions)
+ }
+ return newSpreaderSimple(encode)
+ }
+
+ mkdirerFactory := func(fileExtensions []string) mkdirerSimple {
+ return newMkdirerSimple(fileExtensions)
+ }
+
+ return &treeSimple{
+ grower: growerFactory(
+ conf.lastNodeFormat,
+ conf.intermedialNodeFormat,
+ conf.dryrun,
+ conf.encode,
+ ),
+ spreader: spreaderFactory(
+ conf.encode,
+ conf.dryrun,
+ conf.fileExtensions,
+ ),
+ mkdirer: mkdirerFactory(
+ conf.fileExtensions,
+ ),
+ }
+}
+
+func (t *treeSimple) output(w io.Writer, r io.Reader, conf *config) error {
+ rg := newRootGeneratorSimple(r, conf.space)
+ roots, err := rg.generate()
+ if err != nil {
+ return err
+ }
+
+ if err := t.grow(roots); err != nil {
+ return err
+ }
+ return t.spread(w, roots)
+}
+
+func (t *treeSimple) outputProgrammably(w io.Writer, root *Node, conf *config) error {
+ if err := t.grow([]*Node{root}); err != nil {
+ return err
+ }
+ return t.spread(w, []*Node{root})
+}
+
+func (t *treeSimple) makedir(r io.Reader, conf *config) error {
+ rg := newRootGeneratorSimple(r, conf.space)
+ roots, err := rg.generate()
+ if err != nil {
+ return err
+ }
+
+ if err := t.grow(roots); err != nil {
+ return err
+ }
+ return t.mkdir(roots)
+}
+
+func (t *treeSimple) makedirProgrammably(root *Node, conf *config) error {
+ t.enableValidation()
+ // when detect invalid node name, return error. process end.
+ if err := t.grow([]*Node{root}); err != nil {
+ return err
+ }
+ if conf.dryrun {
+ // when detected no invalid node name, output tree.
+ return t.spread(color.Output, []*Node{root})
+ }
+ // when detected no invalid node name, no output tree.
+ return t.mkdir([]*Node{root})
+}
+
+// 関心事は各ノードの枝の形成
+type growerSimple interface {
+ grow([]*Node) error
+ enableValidation()
+}
+
+// 関心事はtreeの出力
+type spreaderSimple interface {
+ spread(io.Writer, []*Node) error
+}
+
+// 関心事はファイルの生成
+// interfaceを使う必要はないが、growerSimple/spreaderSimpleと合わせたいため
+type mkdirerSimple interface {
+ mkdir([]*Node) error
+}
+
+func (t *treeSimple) grow(roots []*Node) error {
+ return t.grower.grow(roots)
+}
+
+func (t *treeSimple) spread(w io.Writer, roots []*Node) error {
+ return t.spreader.spread(w, roots)
+}
+
+func (t *treeSimple) mkdir(roots []*Node) error {
+ return t.mkdirer.mkdir(roots)
+}
diff --git a/simple_tree_grower.go b/simple_tree_grower.go
new file mode 100644
index 0000000..e9d7532
--- /dev/null
+++ b/simple_tree_grower.go
@@ -0,0 +1,130 @@
+//go:build !wasm
+
+package gtree
+
+func newGrowerSimple(
+ lastNodeFormat, intermedialNodeFormat branchFormat,
+ enabledValidation bool,
+) growerSimple {
+ return &defaultGrowerSimple{
+ lastNodeFormat: lastNodeFormat,
+ intermedialNodeFormat: intermedialNodeFormat,
+ enabledValidation: enabledValidation,
+ }
+}
+
+func newNopGrowerSimple() growerSimple {
+ return &nopGrowerSimple{}
+}
+
+type branchFormat struct {
+ directly, indirectly string
+}
+
+type defaultGrowerSimple struct {
+ lastNodeFormat branchFormat
+ intermedialNodeFormat branchFormat
+ enabledValidation bool
+}
+
+func (dg *defaultGrowerSimple) grow(roots []*Node) error {
+ for _, root := range roots {
+ if err := dg.assemble(root); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (dg *defaultGrowerSimple) assemble(current *Node) error {
+ if err := dg.assembleBranch(current); err != nil {
+ return err
+ }
+
+ for _, child := range current.children {
+ if err := dg.assemble(child); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (dg *defaultGrowerSimple) assembleBranch(current *Node) error {
+ current.clean() // 例えば、MkdirProgrammably funcでrootノードを使いまわすと、前回func実行時に形成されたノードの枝が残ったまま追記されてしまうため。
+
+ dg.assembleBranchDirectly(current)
+
+ // go back to the root to form a branch.
+ tmpParent := current.parent
+ if tmpParent != nil {
+ for ; !tmpParent.isRoot(); tmpParent = tmpParent.parent {
+ dg.assembleBranchIndirectly(current, tmpParent)
+ }
+ }
+
+ dg.assembleBranchFinally(current, tmpParent)
+
+ if dg.enabledValidation {
+ return current.validatePath()
+ }
+ return nil
+}
+
+func (dg *defaultGrowerSimple) assembleBranchDirectly(current *Node) {
+ if current == nil || current.isRoot() {
+ return
+ }
+
+ current.setPath(current.name)
+
+ if current.isLastOfHierarchy() {
+ current.setBranch(current.branch(), dg.lastNodeFormat.directly)
+ } else {
+ current.setBranch(current.branch(), dg.intermedialNodeFormat.directly)
+ }
+}
+
+func (dg *defaultGrowerSimple) assembleBranchIndirectly(current, parent *Node) {
+ if current == nil || parent == nil || current.isRoot() {
+ return
+ }
+
+ current.setPath(parent.name, current.path())
+
+ if parent.isLastOfHierarchy() {
+ current.setBranch(dg.lastNodeFormat.indirectly, current.branch())
+ } else {
+ current.setBranch(dg.intermedialNodeFormat.indirectly, current.branch())
+ }
+}
+
+func (*defaultGrowerSimple) assembleBranchFinally(current, root *Node) {
+ if current == nil {
+ return
+ }
+
+ if root != nil {
+ current.setPath(root.path(), current.path())
+ }
+
+ if current.isRoot() {
+ current.setBranch(current.name, "\n")
+ } else {
+ current.setBranch(current.branch(), " ", current.name, "\n")
+ }
+}
+
+func (dg *defaultGrowerSimple) enableValidation() {
+ dg.enabledValidation = true
+}
+
+type nopGrowerSimple struct{}
+
+func (*nopGrowerSimple) grow(_ []*Node) error { return nil }
+
+func (*nopGrowerSimple) enableValidation() {}
+
+var (
+ _ growerSimple = (*defaultGrowerSimple)(nil)
+ _ growerSimple = (*nopGrowerSimple)(nil)
+)
diff --git a/simple_tree_mkdirer.go b/simple_tree_mkdirer.go
new file mode 100644
index 0000000..7a7dd3e
--- /dev/null
+++ b/simple_tree_mkdirer.go
@@ -0,0 +1,75 @@
+//go:build !wasm
+
+package gtree
+
+import (
+ "os"
+ "strings"
+)
+
+func newMkdirerSimple(fileExtensions []string) mkdirerSimple {
+ return &defaultMkdirerSimple{
+ fileConsiderer: newFileConsiderer(fileExtensions),
+ }
+}
+
+type defaultMkdirerSimple struct {
+ fileConsiderer *fileConsiderer
+}
+
+func (dm *defaultMkdirerSimple) mkdir(roots []*Node) error {
+ if dm.isExistRoot(roots) {
+ return ErrExistPath
+ }
+
+ for _, root := range roots {
+ if err := dm.makeDirectoriesAndFiles(root); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (*defaultMkdirerSimple) isExistRoot(roots []*Node) bool {
+ for _, root := range roots {
+ if _, err := os.Stat(root.path()); !os.IsNotExist(err) {
+ return true
+ }
+ }
+ return false
+}
+
+func (dm *defaultMkdirerSimple) makeDirectoriesAndFiles(current *Node) error {
+ if dm.fileConsiderer.isFile(current) {
+ dir := strings.TrimSuffix(current.path(), current.name)
+ if err := dm.mkdirAll(dir); err != nil {
+ return err
+ }
+ return dm.mkfile(current.path())
+ }
+
+ if !current.hasChild() {
+ return dm.mkdirAll(current.path())
+ }
+
+ for _, child := range current.children {
+ if err := dm.makeDirectoriesAndFiles(child); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (*defaultMkdirerSimple) mkdirAll(dir string) error {
+ return os.MkdirAll(dir, permission)
+}
+
+func (*defaultMkdirerSimple) mkfile(path string) error {
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ return f.Close()
+}
+
+var _ mkdirerSimple = (*defaultMkdirerSimple)(nil)
diff --git a/simple_tree_spreader.go b/simple_tree_spreader.go
new file mode 100644
index 0000000..064a1ab
--- /dev/null
+++ b/simple_tree_spreader.go
@@ -0,0 +1,236 @@
+//go:build !wasm
+
+package gtree
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/fatih/color"
+ toml "github.com/pelletier/go-toml/v2"
+ "gopkg.in/yaml.v3"
+)
+
+func newSpreaderSimple(encode encode) spreaderSimple {
+ switch encode {
+ case encodeJSON:
+ return &jsonSpreaderSimple{}
+ case encodeYAML:
+ return &yamlSpreaderSimple{}
+ case encodeTOML:
+ return &tomlSpreaderSimple{}
+ default:
+ return &defaultSpreaderSimple{}
+ }
+}
+
+func newColorizeSpreaderSimple(fileExtensions []string) spreaderSimple {
+ return &colorizeSpreaderSimple{
+ defaultSpreaderSimple: &defaultSpreaderSimple{},
+
+ fileConsiderer: newFileConsiderer(fileExtensions),
+ fileColor: color.New(color.Bold, color.FgHiCyan),
+ fileCounter: newCounter(),
+
+ dirColor: color.New(color.FgGreen),
+ dirCounter: newCounter(),
+ }
+}
+
+type encode int
+
+const (
+ encodeDefault encode = iota
+ encodeJSON
+ encodeYAML
+ encodeTOML
+)
+
+type defaultSpreaderSimple struct{}
+
+func (ds *defaultSpreaderSimple) spread(w io.Writer, roots []*Node) error {
+ branches := ""
+ for _, root := range roots {
+ branches += ds.spreadBranch(root)
+ }
+ return ds.write(w, branches)
+}
+
+func (*defaultSpreaderSimple) spreadBranch(current *Node) string {
+ ret := current.branch()
+ for _, child := range current.children {
+ ret += (*defaultSpreaderSimple)(nil).spreadBranch(child)
+ }
+ return ret
+}
+
+func (*defaultSpreaderSimple) write(w io.Writer, in string) error {
+ buf := bufio.NewWriter(w)
+ if _, err := buf.WriteString(in); err != nil {
+ return err
+ }
+ return buf.Flush()
+}
+
+type colorizeSpreaderSimple struct {
+ *defaultSpreaderSimple // NOTE: xxx
+
+ fileConsiderer *fileConsiderer
+ fileColor *color.Color
+ fileCounter *counter
+
+ dirColor *color.Color
+ dirCounter *counter
+}
+
+func (cs *colorizeSpreaderSimple) spread(w io.Writer, roots []*Node) error {
+ ret := ""
+ for _, root := range roots {
+ cs.fileCounter.reset()
+ cs.dirCounter.reset()
+ ret += fmt.Sprintf("%s\n%s", cs.spreadBranch(root), cs.summary())
+ }
+ return cs.write(w, ret)
+}
+
+func (cs *colorizeSpreaderSimple) spreadBranch(current *Node) string {
+ cs.colorize(current)
+ ret := current.branch()
+ for _, child := range current.children {
+ ret += cs.spreadBranch(child)
+ }
+ return ret
+}
+
+func (cs *colorizeSpreaderSimple) colorize(current *Node) {
+ if cs.fileConsiderer.isFile(current) {
+ _ = cs.fileCounter.next()
+ current.name = cs.fileColor.Sprint(current.name)
+ } else {
+ _ = cs.dirCounter.next()
+ current.name = cs.dirColor.Sprint(current.name)
+ }
+}
+
+func (cs *colorizeSpreaderSimple) summary() string {
+ return fmt.Sprintf(
+ "%d directories, %d files\n",
+ cs.dirCounter.current(),
+ cs.fileCounter.current(),
+ )
+}
+
+type jsonSpreaderSimple struct{}
+
+func (*jsonSpreaderSimple) spread(w io.Writer, roots []*Node) error {
+ enc := json.NewEncoder(w)
+ for _, root := range roots {
+ jRoot := root.toJSONNode(nil)
+ if err := enc.Encode(jRoot); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type jsonNode struct {
+ Name string `json:"value"`
+ Children []*jsonNode `json:"children"`
+}
+
+func (parent *Node) toJSONNode(jParent *jsonNode) *jsonNode {
+ if jParent == nil {
+ jParent = &jsonNode{Name: parent.name}
+ }
+ if !parent.hasChild() {
+ return jParent
+ }
+
+ jParent.Children = make([]*jsonNode, len(parent.children))
+ for i := range parent.children {
+ jParent.Children[i] = &jsonNode{Name: parent.children[i].name}
+ _ = parent.children[i].toJSONNode(jParent.Children[i])
+ }
+
+ return jParent
+}
+
+type tomlSpreaderSimple struct{}
+
+func (*tomlSpreaderSimple) spread(w io.Writer, roots []*Node) error {
+ enc := toml.NewEncoder(w)
+ for _, root := range roots {
+ tRoot := root.toTOMLNode(nil)
+ if err := enc.Encode(tRoot); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type tomlNode struct {
+ Name string `toml:"value"`
+ Children []*tomlNode `toml:"children"`
+}
+
+func (parent *Node) toTOMLNode(tParent *tomlNode) *tomlNode {
+ if tParent == nil {
+ tParent = &tomlNode{Name: parent.name}
+ }
+ if !parent.hasChild() {
+ return tParent
+ }
+
+ tParent.Children = make([]*tomlNode, len(parent.children))
+ for i := range parent.children {
+ tParent.Children[i] = &tomlNode{Name: parent.children[i].name}
+ _ = parent.children[i].toTOMLNode(tParent.Children[i])
+ }
+
+ return tParent
+}
+
+type yamlSpreaderSimple struct{}
+
+func (*yamlSpreaderSimple) spread(w io.Writer, roots []*Node) error {
+ enc := yaml.NewEncoder(w)
+ for _, root := range roots {
+ yRoot := root.toYAMLNode(nil)
+ if err := enc.Encode(yRoot); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type yamlNode struct {
+ Name string `yaml:"value"`
+ Children []*yamlNode `yaml:"children"`
+}
+
+func (parent *Node) toYAMLNode(yParent *yamlNode) *yamlNode {
+ if yParent == nil {
+ yParent = &yamlNode{Name: parent.name}
+ }
+ if !parent.hasChild() {
+ return yParent
+ }
+
+ yParent.Children = make([]*yamlNode, len(parent.children))
+ for i := range parent.children {
+ yParent.Children[i] = &yamlNode{Name: parent.children[i].name}
+ _ = parent.children[i].toYAMLNode(yParent.Children[i])
+ }
+
+ return yParent
+}
+
+var (
+ _ spreaderSimple = (*defaultSpreaderSimple)(nil)
+ _ spreaderSimple = (*colorizeSpreaderSimple)(nil)
+ _ spreaderSimple = (*jsonSpreaderSimple)(nil)
+ _ spreaderSimple = (*yamlSpreaderSimple)(nil)
+ _ spreaderSimple = (*tomlSpreaderSimple)(nil)
+)
diff --git a/tree.go b/tree.go
index 984485d..023b08c 100644
--- a/tree.go
+++ b/tree.go
@@ -2,98 +2,11 @@
package gtree
-import (
- "context"
- "io"
+import "io"
- "golang.org/x/sync/errgroup"
-)
-
-type tree struct {
- grower grower
- spreader spreader
- mkdirer mkdirer
-}
-
-// 関心事は各ノードの枝の形成
-type grower interface {
- grow(context.Context, <-chan *Node) (<-chan *Node, <-chan error)
- enableValidation()
-}
-
-// 関心事はtreeの出力
-type spreader interface {
- spread(context.Context, io.Writer, <-chan *Node) <-chan error
-}
-
-// 関心事はファイルの生成
-// interfaceを使う必要はないが、grower/spreaderと合わせたいため
-type mkdirer interface {
- mkdir(context.Context, <-chan *Node) <-chan error
-}
-
-func newTree(conf *config) *tree {
- growerFactory := func(lastNodeFormat, intermedialNodeFormat branchFormat, dryrun bool, encode encode) grower {
- if encode != encodeDefault {
- return newNopGrower()
- }
- return newGrower(lastNodeFormat, intermedialNodeFormat, dryrun)
- }
-
- spreaderFactory := func(encode encode, dryrun bool, fileExtensions []string) spreader {
- if dryrun {
- return newColorizeSpreader(fileExtensions)
- }
- return newSpreader(encode)
- }
-
- mkdirerFactory := func(fileExtensions []string) mkdirer {
- return newMkdirer(fileExtensions)
- }
-
- return &tree{
- grower: growerFactory(
- conf.lastNodeFormat,
- conf.intermedialNodeFormat,
- conf.dryrun,
- conf.encode,
- ),
- spreader: spreaderFactory(
- conf.encode,
- conf.dryrun,
- conf.fileExtensions,
- ),
- mkdirer: mkdirerFactory(
- conf.fileExtensions,
- ),
- }
-}
-
-func (t *tree) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) {
- return t.grower.grow(ctx, roots)
-}
-
-func (t *tree) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error {
- return t.spreader.spread(ctx, w, roots)
-}
-
-func (t *tree) mkdir(ctx context.Context, roots <-chan *Node) <-chan error {
- return t.mkdirer.mkdir(ctx, roots)
-}
-
-// パイプラインの全ステージで最初のエラーを返却
-func handlePipelineErr(echs ...<-chan error) error {
- eg, _ := errgroup.WithContext(context.TODO())
- for i := range echs {
- i := i
- eg.Go(func() error {
- for e := range echs[i] {
- if e != nil {
- return e
- }
- }
- return nil
- })
- }
- return eg.Wait()
+type tree interface {
+ output(io.Writer, io.Reader, *config) error
+ outputProgrammably(io.Writer, *Node, *config) error
+ makedir(io.Reader, *config) error
+ makedirProgrammably(*Node, *config) error
}
diff --git a/tree_handler.go b/tree_handler.go
index 5c1057e..6ac2ee2 100644
--- a/tree_handler.go
+++ b/tree_handler.go
@@ -3,7 +3,6 @@
package gtree
import (
- "context"
"io"
)
@@ -14,15 +13,13 @@ func Output(w io.Writer, r io.Reader, options ...Option) error {
return err
}
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
+ if conf.massive {
+ tree := newTreePipeline(conf)
+ return tree.output(w, r, conf)
+ }
- tree := newTree(conf)
- splitStream, errcsl := split(ctx, r)
- rootStream, errcr := newRootGenerator(conf.space).generate(ctx, splitStream)
- growStream, errcg := tree.grow(ctx, rootStream)
- errcs := tree.spread(ctx, w, growStream)
- return handlePipelineErr(errcsl, errcr, errcg, errcs)
+ tree := newTreeSimple(conf)
+ return tree.output(w, r, conf)
}
// Mkdir makes directories.
@@ -32,13 +29,11 @@ func Mkdir(r io.Reader, options ...Option) error {
return err
}
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
+ if conf.massive {
+ tree := newTreePipeline(conf)
+ return tree.makedir(r, conf)
+ }
- tree := newTree(conf)
- splitStream, errcsl := split(ctx, r)
- rootStream, errcr := newRootGenerator(conf.space).generate(ctx, splitStream)
- growStream, errcg := tree.grow(ctx, rootStream)
- errcm := tree.mkdir(ctx, growStream)
- return handlePipelineErr(errcsl, errcr, errcg, errcm)
+ tree := newTreeSimple(conf)
+ return tree.makedir(r, conf)
}
diff --git a/tree_handler_mkdir_test.go b/tree_handler_mkdir_test.go
index 19ceffe..59330af 100644
--- a/tree_handler_mkdir_test.go
+++ b/tree_handler_mkdir_test.go
@@ -108,6 +108,21 @@ func TestMkdir(t *testing.T) {
},
wantErr: nil,
},
+ {
+ name: "case(succeeded/make directories and files/massive root)",
+ in: in{
+ input: strings.NewReader(strings.TrimSpace(`
+- root_j
+ - b.go
+ - bb.go
+ - lll`)),
+ options: []gtree.Option{
+ gtree.WithFileExtensions([]string{".go"}),
+ gtree.WithMassive(),
+ },
+ },
+ wantErr: nil,
+ },
}
for _, tt := range tests {
diff --git a/tree_handler_output_test.go b/tree_handler_output_test.go
index 4d74c4c..0c83d5e 100644
--- a/tree_handler_output_test.go
+++ b/tree_handler_output_test.go
@@ -469,6 +469,126 @@ a prev tab
err: nil,
},
},
+ {
+ // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施
+ name: "case(succeeded/when massive root)",
+ in: in{
+ input: strings.NewReader(strings.TrimSpace(`
+- a
+ - b
+ - c`)),
+ options: []gtree.Option{
+ gtree.WithMassive(),
+ },
+ },
+ out: out{
+ output: strings.TrimPrefix(`
+a
+└── b
+ └── c
+`, "\n"),
+ err: nil,
+ },
+ },
+ {
+ // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施
+ name: "case(succeeded/when massive root and dryrun)",
+ in: in{
+ input: strings.NewReader(strings.TrimSpace(`
+- a
+ - b
+ - z
+ - c
+ - y`)),
+ options: []gtree.Option{
+ gtree.WithMassive(),
+ gtree.WithDryRun(),
+ gtree.WithFileExtensions([]string{"c"}),
+ },
+ },
+ out: out{
+ output: strings.TrimPrefix(`
+a
+├── b
+│ ├── z
+│ └── c
+└── y
+
+4 directories, 1 files
+`, "\n"),
+ err: nil,
+ },
+ },
+ {
+ // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施
+ name: "case(succeeded/when massive root and json)",
+ in: in{
+ input: strings.NewReader(strings.TrimSpace(`
+- a
+ - b
+ - c`)),
+ options: []gtree.Option{
+ gtree.WithMassive(),
+ gtree.WithEncodeJSON(),
+ },
+ },
+ out: out{
+ output: `{"value":"a","children":[{"value":"b","children":[{"value":"c","children":null}]}]}` + "\n",
+ err: nil,
+ },
+ },
+ {
+ // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施
+ name: "case(succeeded/when massive root and yaml)",
+ in: in{
+ input: strings.NewReader(strings.TrimSpace(`
+- a
+ - b
+ - c`)),
+ options: []gtree.Option{
+ gtree.WithMassive(),
+ gtree.WithEncodeYAML(),
+ },
+ },
+ out: out{
+ output: strings.TrimSpace(`
+value: a
+children:
+ - value: b
+ children:
+ - value: c
+ children: []
+`) + "\n",
+ err: nil,
+ },
+ },
+ {
+ // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施
+ name: "case(succeeded/when massive root and toml)",
+ in: in{
+ input: strings.NewReader(strings.TrimSpace(`
+- a
+ - b
+ - c`)),
+ options: []gtree.Option{
+ gtree.WithMassive(),
+ gtree.WithEncodeTOML(),
+ },
+ },
+ out: out{
+ output: strings.TrimSpace(`
+value = 'a'
+
+[[children]]
+value = 'b'
+
+[[children.children]]
+value = 'c'
+children = []
+`) + "\n",
+ err: nil,
+ },
+ },
}
for _, tt := range tests {
diff --git a/tree_handler_programmably.go b/tree_handler_programmably.go
index 0ebece9..ad10698 100644
--- a/tree_handler_programmably.go
+++ b/tree_handler_programmably.go
@@ -3,11 +3,8 @@
package gtree
import (
- "context"
"errors"
"io"
-
- "github.com/fatih/color"
)
var (
@@ -38,18 +35,13 @@ func OutputProgrammably(w io.Writer, root *Node, options ...Option) error {
idxCounter.reset()
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- rootStream := make(chan *Node)
- go func() {
- defer close(rootStream)
- rootStream <- root
- }()
- tree := newTree(conf)
- growStream, errcg := tree.grow(ctx, rootStream)
- errcs := tree.spread(ctx, w, growStream)
- return handlePipelineErr(errcg, errcs)
+ if conf.massive {
+ tree := newTreePipeline(conf)
+ return tree.outputProgrammably(w, root, conf)
+ }
+
+ tree := newTreeSimple(conf)
+ return tree.outputProgrammably(w, root, conf)
}
var (
@@ -74,29 +66,20 @@ func MkdirProgrammably(root *Node, options ...Option) error {
idxCounter.reset()
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- rootStream := make(chan *Node)
- go func() {
- defer close(rootStream)
- rootStream <- root
- }()
- tree := newTree(conf)
- tree.enableValidation()
- // when detect invalid node name, return error. process end.
- growStream, errcg := tree.grow(ctx, rootStream)
- if conf.dryrun {
- // when detected no invalid node name, output tree.
- errcs := tree.spread(ctx, color.Output, growStream)
- return handlePipelineErr(errcg, errcs)
+ if conf.massive {
+ tree := newTreePipeline(conf)
+ return tree.makedirProgrammably(root, conf)
}
- // when detected no invalid node name, no output tree.
- errcm := tree.mkdir(ctx, growStream)
- return handlePipelineErr(errcg, errcm)
+
+ tree := newTreeSimple(conf)
+ return tree.makedirProgrammably(root, conf)
+}
+
+func (t *treeSimple) enableValidation() {
+ t.grower.enableValidation()
}
-func (t *tree) enableValidation() {
+func (t *treePipeline) enableValidation() {
t.grower.enableValidation()
}
diff --git a/tree_handler_programmably_mkdir_test.go b/tree_handler_programmably_mkdir_test.go
index 60b2f3c..1d899a8 100644
--- a/tree_handler_programmably_mkdir_test.go
+++ b/tree_handler_programmably_mkdir_test.go
@@ -19,6 +19,11 @@ func TestMkdirProgrammably(t *testing.T) {
name: "case(succeeded)",
root: prepare(),
},
+ {
+ name: "case(succeeded/massive)",
+ root: prepare_a(),
+ options: []gtree.Option{gtree.WithMassive()},
+ },
{
name: "case(not root)",
root: prepareNotRoot(),
@@ -96,3 +101,9 @@ func prepareExistRoot(t *testing.T) *gtree.Node {
root.Add("temp")
return root
}
+
+func prepare_a() *gtree.Node {
+ root := gtree.NewRoot("root8")
+ root.Add("child 1").Add("child 2")
+ return root
+}
diff --git a/tree_handler_programmably_output_test.go b/tree_handler_programmably_output_test.go
index 9cf9b36..c523d47 100644
--- a/tree_handler_programmably_output_test.go
+++ b/tree_handler_programmably_output_test.go
@@ -50,6 +50,17 @@ func TestOutputProgrammably(t *testing.T) {
root: prepare(),
want: strings.TrimPrefix(`
root
+└── child 1
+ └── child 2
+`, "\n"),
+ wantErr: nil,
+ },
+ {
+ name: "case(succeeded/massive)",
+ root: prepare(),
+ options: []gtree.Option{gtree.WithMassive()},
+ want: strings.TrimPrefix(`
+root
└── child 1
└── child 2
`, "\n"),