ffmd/ffmd.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
}