commit 34293148d7a5b63929ae62fc5f5777ef0b8b9968 Author: jolheiser Date: Fri Oct 29 17:11:34 2021 -0500 Initial commit Signed-off-by: jolheiser diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..433f7db --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c58db40 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# ffmd + +[![Go Reference](https://pkg.go.dev/badge/go.jolheiser.com/ffmd.svg)](https://pkg.go.dev/go.jolheiser.com/ffmd) + +## License + +[MIT](LICENSE) \ No newline at end of file diff --git a/examples/command-sub.md b/examples/command-sub.md new file mode 100644 index 0000000..3af24eb --- /dev/null +++ b/examples/command-sub.md @@ -0,0 +1,367 @@ +# myapp + +myapp + +``` +myapp +├─ sub1 +├─ sub2 +│ ├─ sub3 +│ │ └─ sub4 +│ └─ sub5 +└─ sub6 +``` + + +``` +[--help] +[--myapp-bool-flag-f] +[--myapp-bool-flag-t] +[--myapp-duration-flag]=[value] +[--myapp-duration-flag-default]=[value] +[--myapp-int-flag]=[value] +[--myapp-int-flag-default]=[value] +[--myapp-string-flag]=[value] +[--myapp-string-flag-default]=[value] +``` +**Usage**: + +``` +myapp [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--myapp-bool-flag-f**: Bool flag false + + +**--myapp-bool-flag-t**: Bool flag true (default: `true`) + + +**--myapp-duration-flag**="": Duration flag with no default + + +**--myapp-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--myapp-int-flag**="": Int flag with no default + + +**--myapp-int-flag-default**="": Int flag with default (default: `100`) + + +**--myapp-string-flag**="": String flag with no default + + +**--myapp-string-flag-default**="": String flag with default (default: `string default`) + + +----- + +## sub1 + +sub1 + + +``` +[--help] +[--sub1-bool-flag-f] +[--sub1-bool-flag-t] +[--sub1-duration-flag]=[value] +[--sub1-duration-flag-default]=[value] +[--sub1-int-flag]=[value] +[--sub1-int-flag-default]=[value] +[--sub1-string-flag]=[value] +[--sub1-string-flag-default]=[value] +``` +**Usage**: + +``` +root [FLAGS] sub1 [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--sub1-bool-flag-f**: Bool flag false + + +**--sub1-bool-flag-t**: Bool flag true (default: `true`) + + +**--sub1-duration-flag**="": Duration flag with no default + + +**--sub1-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--sub1-int-flag**="": Int flag with no default + + +**--sub1-int-flag-default**="": Int flag with default (default: `100`) + + +**--sub1-string-flag**="": String flag with no default + + +**--sub1-string-flag-default**="": String flag with default (default: `string default`) + + +----- + +## sub2 + +Short help + + +``` +[--help] +[--sub2-bool-flag-f] +[--sub2-bool-flag-t] +[--sub2-duration-flag]=[value] +[--sub2-duration-flag-default]=[value] +[--sub2-int-flag]=[value] +[--sub2-int-flag-default]=[value] +[--sub2-string-flag]=[value] +[--sub2-string-flag-default]=[value] +``` +**Usage**: + +``` +root [FLAGS] sub2 [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--sub2-bool-flag-f**: Bool flag false + + +**--sub2-bool-flag-t**: Bool flag true (default: `true`) + + +**--sub2-duration-flag**="": Duration flag with no default + + +**--sub2-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--sub2-int-flag**="": Int flag with no default + + +**--sub2-int-flag-default**="": Int flag with default (default: `100`) + + +**--sub2-string-flag**="": String flag with no default + + +**--sub2-string-flag-default**="": String flag with default (default: `string default`) + + +----- + +### sub3 + +Long help + + +``` +[--help] +[--sub3-bool-flag-f] +[--sub3-bool-flag-t] +[--sub3-duration-flag]=[value] +[--sub3-duration-flag-default]=[value] +[--sub3-int-flag]=[value] +[--sub3-int-flag-default]=[value] +[--sub3-string-flag]=[value] +[--sub3-string-flag-default]=[value] +``` +**Usage**: + +``` +root [FLAGS] sub2 [FLAGS] sub3 [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--sub3-bool-flag-f**: Bool flag false + + +**--sub3-bool-flag-t**: Bool flag true (default: `true`) + + +**--sub3-duration-flag**="": Duration flag with no default + + +**--sub3-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--sub3-int-flag**="": Int flag with no default + + +**--sub3-int-flag-default**="": Int flag with default (default: `100`) + + +**--sub3-string-flag**="": String flag with no default + + +**--sub3-string-flag-default**="": String flag with default (default: `string default`) + + +----- + +#### sub4 + +sub4 + + +``` +[--help] +[--sub4-bool-flag-f] +[--sub4-bool-flag-t] +[--sub4-duration-flag]=[value] +[--sub4-duration-flag-default]=[value] +[--sub4-int-flag]=[value] +[--sub4-int-flag-default]=[value] +[--sub4-string-flag]=[value] +[--sub4-string-flag-default]=[value] +``` +**Usage**: + +``` +sub4 [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--sub4-bool-flag-f**: Bool flag false + + +**--sub4-bool-flag-t**: Bool flag true (default: `true`) + + +**--sub4-duration-flag**="": Duration flag with no default + + +**--sub4-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--sub4-int-flag**="": Int flag with no default + + +**--sub4-int-flag-default**="": Int flag with default (default: `100`) + + +**--sub4-string-flag**="": String flag with no default + + +**--sub4-string-flag-default**="": String flag with default (default: `string default`) + + +----- + +### sub5 + +sub5 + + +``` +[--help] +[--sub5-bool-flag-f] +[--sub5-bool-flag-t] +[--sub5-duration-flag]=[value] +[--sub5-duration-flag-default]=[value] +[--sub5-int-flag]=[value] +[--sub5-int-flag-default]=[value] +[--sub5-string-flag]=[value] +[--sub5-string-flag-default]=[value] +``` +**Usage**: + +``` +sub5 [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--sub5-bool-flag-f**: Bool flag false + + +**--sub5-bool-flag-t**: Bool flag true (default: `true`) + + +**--sub5-duration-flag**="": Duration flag with no default + + +**--sub5-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--sub5-int-flag**="": Int flag with no default + + +**--sub5-int-flag-default**="": Int flag with default (default: `100`) + + +**--sub5-string-flag**="": String flag with no default + + +**--sub5-string-flag-default**="": String flag with default (default: `string default`) + + +----- + +## sub6 + +sub6 + + +``` +[--help] +[--sub6-bool-flag-f] +[--sub6-bool-flag-t] +[--sub6-duration-flag]=[value] +[--sub6-duration-flag-default]=[value] +[--sub6-int-flag]=[value] +[--sub6-int-flag-default]=[value] +[--sub6-string-flag]=[value] +[--sub6-string-flag-default]=[value] +``` +**Usage**: + +``` +sub6 [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--sub6-bool-flag-f**: Bool flag false + + +**--sub6-bool-flag-t**: Bool flag true (default: `true`) + + +**--sub6-duration-flag**="": Duration flag with no default + + +**--sub6-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--sub6-int-flag**="": Int flag with no default + + +**--sub6-int-flag-default**="": Int flag with default (default: `100`) + + +**--sub6-string-flag**="": String flag with no default + + +**--sub6-string-flag-default**="": String flag with default (default: `string default`) + + +----- + diff --git a/examples/command.md b/examples/command.md new file mode 100644 index 0000000..dfc5db3 --- /dev/null +++ b/examples/command.md @@ -0,0 +1,55 @@ +# myapp + +myapp + +``` +myapp +``` + + +``` +[--help] +[--myapp-bool-flag-f] +[--myapp-bool-flag-t] +[--myapp-duration-flag]=[value] +[--myapp-duration-flag-default]=[value] +[--myapp-int-flag]=[value] +[--myapp-int-flag-default]=[value] +[--myapp-string-flag]=[value] +[--myapp-string-flag-default]=[value] +``` +**Usage**: + +``` +myapp [FLAGS] [ARGS...] +``` + +**--help**: Show help + + +**--myapp-bool-flag-f**: Bool flag false + + +**--myapp-bool-flag-t**: Bool flag true (default: `true`) + + +**--myapp-duration-flag**="": Duration flag with no default + + +**--myapp-duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--myapp-int-flag**="": Int flag with no default + + +**--myapp-int-flag-default**="": Int flag with default (default: `100`) + + +**--myapp-string-flag**="": String flag with no default + + +**--myapp-string-flag-default**="": String flag with default (default: `string default`) + + +----- + diff --git a/examples/examples.go b/examples/examples.go new file mode 100644 index 0000000..652e362 --- /dev/null +++ b/examples/examples.go @@ -0,0 +1,121 @@ +//go:build generate +// +build generate + +package main + +import ( + "flag" + "fmt" + "os" + "time" + + "github.com/peterbourgon/ff/v3/ffcli" + + "go.jolheiser.com/ffmd" +) + +//go:generate go run examples.go +func main() { + fs := flagSet("") + md, err := ffmd.FromFlagSet(fs) + if err != nil { + panic(err) + } + write("flagset.md", md) + + command() + commandSub() +} + +func write(path, content string) { + fi, err := os.Create(path) + if err != nil { + panic(err) + } + defer fi.Close() + if _, err := fi.WriteString(content); err != nil { + panic(err) + } +} + +func flagSet(name string) *flag.FlagSet { + fs := flag.NewFlagSet("myapp", flag.ExitOnError) + fs.String(fmt.Sprintf("%sstring-flag", name), "", "String flag with no default") + fs.String(fmt.Sprintf("%sstring-flag-default", name), "string default", "String flag with default") + fs.Int(fmt.Sprintf("%sint-flag", name), 0, "Int flag with no default") + fs.Int(fmt.Sprintf("%sint-flag-default", name), 100, "Int flag with default") + fs.Bool(fmt.Sprintf("%sbool-flag-f", name), false, "Bool flag false") + fs.Bool(fmt.Sprintf("%sbool-flag-t", name), true, "Bool flag true") + fs.Duration(fmt.Sprintf("%sduration-flag", name), 0, "Duration flag with no default") + fs.Duration(fmt.Sprintf("%sduration-flag-default", name), time.Minute*5, "Duration flag with default") + return fs +} + +func command() { + fs1 := flagSet("myapp-") + cmd1 := &ffcli.Command{ + Name: "myapp", + FlagSet: fs1, + Subcommands: nil, + } + + md, err := ffmd.FromCommand(cmd1) + if err != nil { + panic(err) + } + write("command.md", md) +} + +func commandSub() { + fs := flagSet("myapp-") + cmd := &ffcli.Command{ + Name: "myapp", + FlagSet: fs, + } + fs1 := flagSet("sub1-") + cmd1 := &ffcli.Command{ + Name: "sub1", + ShortUsage: "root [FLAGS] sub1 [FLAGS] [ARGS...]", + FlagSet: fs1, + } + fs2 := flagSet("sub2-") + cmd2 := &ffcli.Command{ + Name: "sub2", + ShortUsage: "root [FLAGS] sub2 [FLAGS] [ARGS...]", + ShortHelp: "Short help", + FlagSet: fs2, + } + fs3 := flagSet("sub3-") + cmd3 := &ffcli.Command{ + Name: "sub3", + ShortUsage: "root [FLAGS] sub2 [FLAGS] sub3 [FLAGS] [ARGS...]", + ShortHelp: "Short help", + LongHelp: "Long help", + FlagSet: fs3, + } + fs4 := flagSet("sub4-") + cmd4 := &ffcli.Command{ + Name: "sub4", + FlagSet: fs4, + } + fs5 := flagSet("sub5-") + cmd5 := &ffcli.Command{ + Name: "sub5", + FlagSet: fs5, + } + fs6 := flagSet("sub6-") + cmd6 := &ffcli.Command{ + Name: "sub6", + FlagSet: fs6, + } + + cmd.Subcommands = []*ffcli.Command{cmd1, cmd2, cmd6} + cmd2.Subcommands = []*ffcli.Command{cmd3, cmd5} + cmd3.Subcommands = []*ffcli.Command{cmd4} + + md, err := ffmd.FromCommand(cmd) + if err != nil { + panic(err) + } + write("command-sub.md", md) +} diff --git a/examples/flagset.md b/examples/flagset.md new file mode 100644 index 0000000..1641b75 --- /dev/null +++ b/examples/flagset.md @@ -0,0 +1,45 @@ +## myapp + + +``` +[--bool-flag-f] +[--bool-flag-t] +[--duration-flag]=[value] +[--duration-flag-default]=[value] +[--help] +[--int-flag]=[value] +[--int-flag-default]=[value] +[--string-flag]=[value] +[--string-flag-default]=[value] +``` +**Usage**: + +``` +myapp [FLAGS] [ARGS...] +``` + +**--bool-flag-f**: Bool flag false + + +**--bool-flag-t**: Bool flag true (default: `true`) + + +**--duration-flag**="": Duration flag with no default + + +**--duration-flag-default**="": Duration flag with default (default: `5m0s`) + + +**--help**: Show help + + +**--int-flag**="": Int flag with no default + + +**--int-flag-default**="": Int flag with default (default: `100`) + + +**--string-flag**="": String flag with no default + + +**--string-flag-default**="": String flag with default (default: `string default`) diff --git a/ffmd.go b/ffmd.go new file mode 100644 index 0000000..eaf7da3 --- /dev/null +++ b/ffmd.go @@ -0,0 +1,146 @@ +package ffmd + +import ( + "bytes" + _ "embed" + "flag" + "fmt" + "reflect" + "sort" + "strings" + "text/template" + + "github.com/peterbourgon/ff/v3/ffcli" +) + +var ( + //go:embed ffmd.tmpl + ffmdtmpl string + Template = template.Must(template.New("").Parse(ffmdtmpl)) +) + +// FromCommand turns a ffcli.Command into markdown +func FromCommand(cmd *ffcli.Command) (string, error) { + return fromCommand(cmd, 1) +} + +// FromFlagSet turns a flag.FlagSet into markdown +func FromFlagSet(fs *flag.FlagSet) (string, error) { + return flagSetCommand(fs, 2).Markdown() +} + +func fromCommand(cmd *ffcli.Command, section int) (string, error) { + c := flagSetCommand(cmd.FlagSet, section) + c.Name = cmd.Name + c.Usage = fmt.Sprintf("%s [FLAGS] [ARGS...]", cmd.Name) + if cmd.ShortUsage != "" { + c.Usage = cmd.ShortUsage + } + c.Description = c.Name + if cmd.ShortHelp != "" { + c.Description = cmd.ShortHelp + } + if cmd.LongHelp != "" { + c.Description = cmd.LongHelp + } + // Only top-level gets a tree + if section == 1 { + c.Tree = Tree(cmd) + } + + var md strings.Builder + s, err := c.Markdown() + if err != nil { + return "", err + } + md.WriteString(s) + md.WriteString("\n\n-----\n\n") + for _, sub := range cmd.Subcommands { + s, err = fromCommand(sub, section+1) + md.WriteString(s) + } + + return md.String(), nil +} + +func flagSetCommand(fs *flag.FlagSet, section int) command { + a := command{ + Section: section, + Name: fs.Name(), + Description: "", + Usage: fmt.Sprintf("%s [FLAGS] [ARGS...]", fs.Name()), + Flags: []appFlag{ + { + Name: "help", + Usage: "Show help", + IsBool: true, + }, + }, + } + fs.VisitAll(func(f *flag.Flag) { + _, isBool := f.Value.(boolFlag) + def := f.DefValue + if isZeroValue(f, def) { + def = "" + } + a.Flags = append(a.Flags, appFlag{ + Name: f.Name, + Usage: f.Usage, + Default: def, + IsBool: isBool, + }) + }) + sort.Slice(a.Flags, func(i, j int) bool { + return a.Flags[i].Name < a.Flags[j].Name + }) + return a +} + +type command struct { + Section int + Name string + Description string + Usage string + Flags []appFlag + Tree string +} + +func (c command) Header() string { + return strings.Repeat("#", c.Section) +} + +func (c command) Markdown() (string, error) { + var buf bytes.Buffer + if err := Template.Execute(&buf, c); err != nil { + return "", err + } + return buf.String(), nil +} + +type appFlag struct { + Name string + Usage string + Default string + IsBool bool +} + +// From stdlib + +func isZeroValue(f *flag.Flag, value string) bool { + // Build a zero value of the flag's Value type, and see if the + // result of calling its String method equals the value passed in. + // This works unless the Value type is itself an interface type. + typ := reflect.TypeOf(f.Value) + var z reflect.Value + if typ.Kind() == reflect.Ptr { + z = reflect.New(typ.Elem()) + } else { + z = reflect.Zero(typ) + } + return value == z.Interface().(flag.Value).String() +} + +type boolFlag interface { + flag.Value + IsBoolFlag() bool +} diff --git a/ffmd.tmpl b/ffmd.tmpl new file mode 100644 index 0000000..1139971 --- /dev/null +++ b/ffmd.tmpl @@ -0,0 +1,26 @@ +{{.Header}} {{.Name}} +{{- if .Description}} + +{{.Description}} +{{- end}} +{{if .Tree}} +``` +{{.Tree}} +``` +{{end}} + +```{{range $flag := .Flags}} +[--{{$flag.Name}}]{{if not $flag.IsBool}}=[value]{{end}} +{{- end}} +``` +{{- if .Usage}} +**Usage**: + +``` +{{.Usage}} +``` +{{- end -}} +{{range $flag := .Flags}} + +**--{{$flag.Name}}**{{if not $flag.IsBool}}=""{{end}}: {{$flag.Usage}}{{if $flag.Default}} (default: `{{$flag.Default}}`){{end}} +{{end}} \ No newline at end of file diff --git a/ffmd_test.go b/ffmd_test.go new file mode 100644 index 0000000..9b7a9ef --- /dev/null +++ b/ffmd_test.go @@ -0,0 +1 @@ +package ffmd diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1204da9 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module go.jolheiser.com/ffmd + +go 1.16 + +require ( + github.com/matryer/is v1.4.0 + github.com/peterbourgon/ff/v3 v3.1.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7706950 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM= +github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/tree.go b/tree.go new file mode 100644 index 0000000..f12e61d --- /dev/null +++ b/tree.go @@ -0,0 +1,96 @@ +package ffmd + +import ( + "fmt" + "github.com/peterbourgon/ff/v3/ffcli" + "sort" + "strings" +) + +// Tree returns a tree-representation of a ffcli.Command +func Tree(cmd *ffcli.Command) string { + t := tree{} + for _, path := range commandPaths(cmd, "") { + t.add(path) + } + s := t.String(true, "") + return strings.TrimSpace(s) +} + +func commandPaths(cmd *ffcli.Command, path string) []string { + root := fmt.Sprintf("%s%s", path, cmd.Name) + s := []string{root} + for _, sub := range cmd.Subcommands { + s = append(s, commandPaths(sub, root+"/")...) + } + sort.SliceStable(s, func(i, j int) bool { + return s[i] < s[j] + }) + return s +} + +type tree map[string]tree + +func (t tree) add(path string) { + t.addParts(strings.Split(path, "/")) +} + +func (t tree) addParts(parts []string) { + if len(parts) == 0 { + return + } + next, ok := t[parts[0]] + if !ok { + next = tree{} + t[parts[0]] = next + } + next.addParts(parts[1:]) +} + +func (t tree) keys() []string { + k := make([]string, 0, len(t)) + for key, _ := range t { + k = append(k, key) + } + sort.Strings(k) + return k +} + +func (t tree) String(root bool, padding string) string { + var s strings.Builder + index := 0 + for _, k := range t.keys() { + v := t[k] + s.WriteString(fmt.Sprintf("%s%s\n", padding+pipePad(root, pipe(index, len(t))), k)) + s.WriteString(v.String(false, padding+pipePad(root, outerPipe(index, len(t))))) + index++ + } + return s.String() +} + +func pipe(index int, len int) string { + switch { + case index+1 == len: + return "└─" + case index+1 > len: + return " " + default: + return "├─" + } +} + +func outerPipe(index int, len int) string { + switch { + case index+1 == len: + return " " + default: + return "│ " + } +} + +func pipePad(root bool, box string) string { + if root { + return "" + } + return box + " " +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..a30c92a --- /dev/null +++ b/tree_test.go @@ -0,0 +1,57 @@ +package ffmd + +import ( + "testing" + + "github.com/matryer/is" + "github.com/peterbourgon/ff/v3/ffcli" +) + +func TestTree(t *testing.T) { + is := is.NewRelaxed(t) + + treeSingle := Tree(cmdSingle) + is.Equal(treeSingle, singleTree) // single command tree + + treeSub := Tree(cmdRoot) + is.Equal(treeSub, subTree) // multi-command tree +} + +var ( + cmdSingle = &ffcli.Command{ + Name: "myapp", + } + sub1 = &ffcli.Command{ + Name: "sub1", + } + sub4 = &ffcli.Command{ + Name: "sub4", + } + sub5 = &ffcli.Command{ + Name: "sub5", + } + sub6 = &ffcli.Command{ + Name: "sub6", + } + sub3 = &ffcli.Command{ + Name: "sub3", + Subcommands: []*ffcli.Command{sub4}, + } + sub2 = &ffcli.Command{ + Name: "sub2", + Subcommands: []*ffcli.Command{sub3, sub5}, + } + cmdRoot = &ffcli.Command{ + Name: "myapp", + Subcommands: []*ffcli.Command{sub1, sub2, sub6}, + } + + singleTree = `myapp` + subTree = `myapp +├─ sub1 +├─ sub2 +│ ├─ sub3 +│ │ └─ sub4 +│ └─ sub5 +└─ sub6` +)