package cli import ( "bytes" "fmt" "io" "sort" "strings" "text/template" "github.com/cpuguy83/go-md2man/v2/md2man" ) // ToMarkdown creates a markdown string for the `*App` // The function errors if either parsing or writing of the string fails. func (a *App) ToMarkdown() (string, error) { var w bytes.Buffer if err := a.writeDocTemplate(&w, 0); err != nil { return "", err } return w.String(), nil } // ToMan creates a man page string with section number for the `*App` // The function errors if either parsing or writing of the string fails. func (a *App) ToManWithSection(sectionNumber int) (string, error) { var w bytes.Buffer if err := a.writeDocTemplate(&w, sectionNumber); err != nil { return "", err } man := md2man.Render(w.Bytes()) return string(man), nil } // ToMan creates a man page string for the `*App` // The function errors if either parsing or writing of the string fails. func (a *App) ToMan() (string, error) { man, err := a.ToManWithSection(8) return man, err } type cliTemplate struct { App *App SectionNum int Commands []string GlobalArgs []string SynopsisArgs []string } func (a *App) writeDocTemplate(w io.Writer, sectionNum int) error { const name = "cli" t, err := template.New(name).Parse(MarkdownDocTemplate) if err != nil { return err } return t.ExecuteTemplate(w, name, &cliTemplate{ App: a, SectionNum: sectionNum, Commands: prepareCommands(a.Commands, 0), GlobalArgs: prepareArgsWithValues(a.VisibleFlags()), SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()), }) } func prepareCommands(commands []*Command, level int) []string { var coms []string for _, command := range commands { if command.Hidden { continue } usageText := prepareUsageText(command) usage := prepareUsage(command, usageText) prepared := fmt.Sprintf("%s %s\n\n%s%s", strings.Repeat("#", level+2), strings.Join(command.Names(), ", "), usage, usageText, ) flags := prepareArgsWithValues(command.Flags) if len(flags) > 0 { prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n")) } coms = append(coms, prepared) // recursevly iterate subcommands if len(command.Subcommands) > 0 { coms = append( coms, prepareCommands(command.Subcommands, level+1)..., ) } } return coms } func prepareArgsWithValues(flags []Flag) []string { return prepareFlags(flags, ", ", "**", "**", `""`, true) } func prepareArgsSynopsis(flags []Flag) []string { return prepareFlags(flags, "|", "[", "]", "[value]", false) } func prepareFlags( flags []Flag, sep, opener, closer, value string, addDetails bool, ) []string { args := []string{} for _, f := range flags { flag, ok := f.(DocGenerationFlag) if !ok { continue } modifiedArg := opener for _, s := range flag.Names() { trimmed := strings.TrimSpace(s) if len(modifiedArg) > len(opener) { modifiedArg += sep } if len(trimmed) > 1 { modifiedArg += fmt.Sprintf("--%s", trimmed) } else { modifiedArg += fmt.Sprintf("-%s", trimmed) } } modifiedArg += closer if flag.TakesValue() { modifiedArg += fmt.Sprintf("=%s", value) } if addDetails { modifiedArg += flagDetails(flag) } args = append(args, modifiedArg+"\n") } sort.Strings(args) return args } // flagDetails returns a string containing the flags metadata func flagDetails(flag DocGenerationFlag) string { description := flag.GetUsage() value := flag.GetValue() if value != "" { description += " (default: " + value + ")" } return ": " + description } func prepareUsageText(command *Command) string { if command.UsageText == "" { return "" } // Remove leading and trailing newlines preparedUsageText := strings.Trim(command.UsageText, "\n") var usageText string if strings.Contains(preparedUsageText, "\n") { // Format multi-line string as a code block using the 4 space schema to allow for embedded markdown such // that it will not break the continuous code block. for _, ln := range strings.Split(preparedUsageText, "\n") { usageText += fmt.Sprintf(" %s\n", ln) } } else { // Style a single line as a note usageText = fmt.Sprintf(">%s\n", preparedUsageText) } return usageText } func prepareUsage(command *Command, usageText string) string { if command.Usage == "" { return "" } usage := command.Usage + "\n" // Add a newline to the Usage IFF there is a UsageText if usageText != "" { usage += "\n" } return usage }