package docs
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/cpuguy83/go-md2man/v2/md2man"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// GenManTree will generate a man page for this command and all descendants
// in the directory given. The header may be nil. This function may not work
// correctly if your command names have `-` in them. If you have `cmd` with two
// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third`
// it is undefined which help output will be in the file `cmd-sub-third.1`.
func GenManTree(cmd *cobra.Command, dir string) error {
return GenManTreeFromOpts(cmd, GenManTreeOptions{
Path: dir,
CommandSeparator: "-",
})
}
// GenManTreeFromOpts generates a man page for the command and all descendants.
// The pages are written to the opts.Path directory.
func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
if err := GenManTreeFromOpts(c, opts); err != nil {
return err
}
}
section := "1"
separator := "_"
if opts.CommandSeparator != "" {
separator = opts.CommandSeparator
}
basename := strings.Replace(cmd.CommandPath(), " ", separator, -1)
filename := filepath.Join(opts.Path, basename+"."+section)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
var versionString string
if v := os.Getenv("GH_VERSION"); v != "" {
versionString = "GitHub CLI " + v
}
return GenMan(cmd, &GenManHeader{
Section: section,
Source: versionString,
Manual: "GitHub CLI manual",
}, f)
}
// GenManTreeOptions is the options for generating the man pages.
// Used only in GenManTreeFromOpts.
type GenManTreeOptions struct {
Path string
CommandSeparator string
}
// GenManHeader is a lot like the .TH header at the start of man pages. These
// include the title, section, date, source, and manual. We will use the
// current time if Date is unset.
type GenManHeader struct {
Title string
Section string
Date *time.Time
Source string
Manual string
}
// GenMan will generate a man page for the given command and write it to
// w. The header argument may be nil, however obviously w may not.
func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
if err := fillHeader(header, cmd.CommandPath()); err != nil {
return err
}
b := genMan(cmd, header)
_, err := w.Write(md2man.Render(b))
return err
}
func fillHeader(header *GenManHeader, name string) error {
if header.Title == "" {
header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
}
if header.Section == "" {
header.Section = "1"
}
if header.Date == nil {
now := time.Now()
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
if err != nil {
return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
}
now = time.Unix(unixEpoch, 0)
}
header.Date = &now
}
return nil
}
func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
buf.WriteString(fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s"
# NAME
`, header.Title, header.Section, header.Date.Format("Jan 2006"), header.Source, header.Manual))
buf.WriteString(fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short))
buf.WriteString("# SYNOPSIS\n")
buf.WriteString(fmt.Sprintf("`%s`\n\n", cmd.UseLine()))
if cmd.Long != "" && cmd.Long != cmd.Short {
buf.WriteString("# DESCRIPTION\n")
buf.WriteString(cmd.Long + "\n\n")
}
}
func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
flags.VisitAll(func(flag *pflag.Flag) {
if len(flag.Deprecated) > 0 || flag.Hidden || flag.Name == "help" {
return
}
varname, usage := pflag.UnquoteUsage(flag)
if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
buf.WriteString(fmt.Sprintf("`-%s`, `--%s`", flag.Shorthand, flag.Name))
} else {
buf.WriteString(fmt.Sprintf("`--%s`", flag.Name))
}
if varname == "" {
buf.WriteString("\n")
} else {
buf.WriteString(fmt.Sprintf(" `<%s>`\n", varname))
}
buf.WriteString(fmt.Sprintf(": %s\n\n", usage))
})
}
func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
flags := command.NonInheritedFlags()
if flags.HasAvailableFlags() {
buf.WriteString("# OPTIONS\n")
manPrintFlags(buf, flags)
buf.WriteString("\n")
}
flags = command.InheritedFlags()
if hasNonHelpFlags(flags) {
buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n")
manPrintFlags(buf, flags)
buf.WriteString("\n")
}
}
func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
// something like `rootcmd-subcmd1-subcmd2`
dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1)
buf := new(bytes.Buffer)
manPreamble(buf, header, cmd, dashCommandName)
for _, g := range subcommandGroups(cmd) {
if len(g.Commands) == 0 {
continue
}
fmt.Fprintf(buf, "# %s\n", strings.ToUpper(g.Name))
for _, subcmd := range g.Commands {
fmt.Fprintf(buf, "`%s`\n: %s\n\n", manLink(subcmd), subcmd.Short)
}
}
manPrintOptions(buf, cmd)
if len(cmd.Example) > 0 {
buf.WriteString("# EXAMPLE\n")
buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
}
if cmd.HasParent() {
buf.WriteString("# SEE ALSO\n")
buf.WriteString(fmt.Sprintf("`%s`\n", manLink(cmd.Parent())))
}
return buf.Bytes()
}
func manLink(cmd *cobra.Command) string {
p := cmd.CommandPath()
return fmt.Sprintf("%s(%d)", strings.Replace(p, " ", "-", -1), 1)
}