174 lines
3.5 KiB
Go
174 lines
3.5 KiB
Go
package ffmd
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"errors"
|
|
"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))
|
|
)
|
|
|
|
// Command turns a ffcli.Command into markdown
|
|
func Command(cmd *ffcli.Command) (string, error) {
|
|
return fromCommand(cmd, 1)
|
|
}
|
|
|
|
// FlagSet turns a flag.FlagSet into markdown
|
|
func FlagSet(fs *flag.FlagSet) (string, error) {
|
|
return flagSetCommand(fs, 2).Markdown()
|
|
}
|
|
|
|
func fromCommand(cmd *ffcli.Command, section int) (string, error) {
|
|
if cmd.FlagSet == nil {
|
|
return "", errors.New("all commands should have a flagset, even if empty")
|
|
}
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
aliases := make(map[string][]string)
|
|
fs.VisitAll(func(f *flag.Flag) {
|
|
_, isBool := f.Value.(boolFlag)
|
|
def := f.DefValue
|
|
if isZeroValue(f, def) {
|
|
def = ""
|
|
}
|
|
af := appFlag{
|
|
Name: f.Name,
|
|
Usage: f.Usage,
|
|
Default: def,
|
|
IsBool: isBool,
|
|
}
|
|
if strings.HasPrefix(af.Usage, "--") {
|
|
aliasOf := strings.TrimPrefix(af.Usage, "--")
|
|
if _, ok := aliases[aliasOf]; !ok {
|
|
aliases[aliasOf] = make([]string, 0)
|
|
}
|
|
aliases[aliasOf] = append(aliases[aliasOf], af.Name)
|
|
return
|
|
}
|
|
a.Flags = append(a.Flags, af)
|
|
})
|
|
for idx, f := range a.Flags {
|
|
f.Aliases = aliases[f.Name]
|
|
a.Flags[idx] = f
|
|
}
|
|
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
|
|
Aliases []string
|
|
Usage string
|
|
Default string
|
|
IsBool bool
|
|
}
|
|
|
|
func (a appFlag) AllNames() string {
|
|
names := []string{"--" + a.Name}
|
|
for _, alias := range a.Aliases {
|
|
names = append(names, "-"+alias)
|
|
}
|
|
return strings.Join(names, ",")
|
|
}
|
|
|
|
// 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
|
|
}
|