github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/internal/docs/man.go (about)

     1  package docs
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/cpuguy83/go-md2man/v2/md2man"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/pflag"
    16  )
    17  
    18  // GenManTree will generate a man page for this command and all descendants
    19  // in the directory given. The header may be nil. This function may not work
    20  // correctly if your command names have `-` in them. If you have `cmd` with two
    21  // subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third`
    22  // it is undefined which help output will be in the file `cmd-sub-third.1`.
    23  func GenManTree(cmd *cobra.Command, dir string) error {
    24  	return GenManTreeFromOpts(cmd, GenManTreeOptions{
    25  		Path:             dir,
    26  		CommandSeparator: "-",
    27  	})
    28  }
    29  
    30  // GenManTreeFromOpts generates a man page for the command and all descendants.
    31  // The pages are written to the opts.Path directory.
    32  func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
    33  	for _, c := range cmd.Commands() {
    34  		if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
    35  			continue
    36  		}
    37  		if err := GenManTreeFromOpts(c, opts); err != nil {
    38  			return err
    39  		}
    40  	}
    41  
    42  	section := "1"
    43  	separator := "_"
    44  	if opts.CommandSeparator != "" {
    45  		separator = opts.CommandSeparator
    46  	}
    47  	basename := strings.Replace(cmd.CommandPath(), " ", separator, -1)
    48  	filename := filepath.Join(opts.Path, basename+"."+section)
    49  	f, err := os.Create(filename)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	defer f.Close()
    54  
    55  	var versionString string
    56  	if v := os.Getenv("GH_VERSION"); v != "" {
    57  		versionString = "GitHub CLI " + v
    58  	}
    59  
    60  	return GenMan(cmd, &GenManHeader{
    61  		Section: section,
    62  		Source:  versionString,
    63  		Manual:  "GitHub CLI manual",
    64  	}, f)
    65  }
    66  
    67  // GenManTreeOptions is the options for generating the man pages.
    68  // Used only in GenManTreeFromOpts.
    69  type GenManTreeOptions struct {
    70  	Path             string
    71  	CommandSeparator string
    72  }
    73  
    74  // GenManHeader is a lot like the .TH header at the start of man pages. These
    75  // include the title, section, date, source, and manual. We will use the
    76  // current time if Date is unset.
    77  type GenManHeader struct {
    78  	Title   string
    79  	Section string
    80  	Date    *time.Time
    81  	Source  string
    82  	Manual  string
    83  }
    84  
    85  // GenMan will generate a man page for the given command and write it to
    86  // w. The header argument may be nil, however obviously w may not.
    87  func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
    88  	if err := fillHeader(header, cmd.CommandPath()); err != nil {
    89  		return err
    90  	}
    91  
    92  	b := genMan(cmd, header)
    93  
    94  	_, err := w.Write(md2man.Render(b))
    95  	return err
    96  }
    97  
    98  func fillHeader(header *GenManHeader, name string) error {
    99  	if header.Title == "" {
   100  		header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
   101  	}
   102  	if header.Section == "" {
   103  		header.Section = "1"
   104  	}
   105  	if header.Date == nil {
   106  		now := time.Now()
   107  		if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
   108  			unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
   109  			if err != nil {
   110  				return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
   111  			}
   112  			now = time.Unix(unixEpoch, 0)
   113  		}
   114  		header.Date = &now
   115  	}
   116  	return nil
   117  }
   118  
   119  func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
   120  	buf.WriteString(fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s"
   121  # NAME
   122  `, header.Title, header.Section, header.Date.Format("Jan 2006"), header.Source, header.Manual))
   123  	buf.WriteString(fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short))
   124  	buf.WriteString("# SYNOPSIS\n")
   125  	buf.WriteString(fmt.Sprintf("`%s`\n\n", cmd.UseLine()))
   126  
   127  	if cmd.Long != "" && cmd.Long != cmd.Short {
   128  		buf.WriteString("# DESCRIPTION\n")
   129  		buf.WriteString(cmd.Long + "\n\n")
   130  	}
   131  }
   132  
   133  func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
   134  	flags.VisitAll(func(flag *pflag.Flag) {
   135  		if len(flag.Deprecated) > 0 || flag.Hidden || flag.Name == "help" {
   136  			return
   137  		}
   138  		varname, usage := pflag.UnquoteUsage(flag)
   139  		if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
   140  			buf.WriteString(fmt.Sprintf("`-%s`, `--%s`", flag.Shorthand, flag.Name))
   141  		} else {
   142  			buf.WriteString(fmt.Sprintf("`--%s`", flag.Name))
   143  		}
   144  		if varname == "" {
   145  			buf.WriteString("\n")
   146  		} else {
   147  			buf.WriteString(fmt.Sprintf(" `<%s>`\n", varname))
   148  		}
   149  		buf.WriteString(fmt.Sprintf(":   %s\n\n", usage))
   150  	})
   151  }
   152  
   153  func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
   154  	flags := command.NonInheritedFlags()
   155  	if flags.HasAvailableFlags() {
   156  		buf.WriteString("# OPTIONS\n")
   157  		manPrintFlags(buf, flags)
   158  		buf.WriteString("\n")
   159  	}
   160  	flags = command.InheritedFlags()
   161  	if hasNonHelpFlags(flags) {
   162  		buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n")
   163  		manPrintFlags(buf, flags)
   164  		buf.WriteString("\n")
   165  	}
   166  }
   167  
   168  func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
   169  	cmd.InitDefaultHelpCmd()
   170  	cmd.InitDefaultHelpFlag()
   171  
   172  	// something like `rootcmd-subcmd1-subcmd2`
   173  	dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1)
   174  
   175  	buf := new(bytes.Buffer)
   176  
   177  	manPreamble(buf, header, cmd, dashCommandName)
   178  	for _, g := range subcommandGroups(cmd) {
   179  		if len(g.Commands) == 0 {
   180  			continue
   181  		}
   182  		fmt.Fprintf(buf, "# %s\n", strings.ToUpper(g.Name))
   183  		for _, subcmd := range g.Commands {
   184  			fmt.Fprintf(buf, "`%s`\n:   %s\n\n", manLink(subcmd), subcmd.Short)
   185  		}
   186  	}
   187  	manPrintOptions(buf, cmd)
   188  	if len(cmd.Example) > 0 {
   189  		buf.WriteString("# EXAMPLE\n")
   190  		buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
   191  	}
   192  	if cmd.HasParent() {
   193  		buf.WriteString("# SEE ALSO\n")
   194  		buf.WriteString(fmt.Sprintf("`%s`\n", manLink(cmd.Parent())))
   195  	}
   196  	return buf.Bytes()
   197  }
   198  
   199  func manLink(cmd *cobra.Command) string {
   200  	p := cmd.CommandPath()
   201  	return fmt.Sprintf("%s(%d)", strings.Replace(p, " ", "-", -1), 1)
   202  }