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 }