commit
34293148d7
|
@ -0,0 +1 @@
|
|||
.idea/
|
|
@ -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.
|
|
@ -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)
|
|
@ -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`)
|
||||
|
||||
|
||||
-----
|
||||
|
|
@ -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`)
|
||||
|
||||
|
||||
-----
|
||||
|
|
@ -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)
|
||||
}
|
|
@ -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`)
|
|
@ -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
|
||||
}
|
|
@ -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}}
|
|
@ -0,0 +1 @@
|
|||
package ffmd
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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 + " "
|
||||
}
|
|
@ -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`
|
||||
)
|
Loading…
Reference in New Issue