github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/internal/docs/man.go (about)

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