Internalize doc generation from Cobra
This commit is contained in:
parent
d7d5a6b753
commit
36949e263a
8 changed files with 835 additions and 5 deletions
|
|
@ -5,15 +5,14 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/internal/docs"
|
||||
"github.com/cli/cli/pkg/cmd/root"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var flagError pflag.ErrorHandling
|
||||
docCmd := pflag.NewFlagSet("", flagError)
|
||||
manPage := docCmd.BoolP("man-page", "", false, "Generate manual pages")
|
||||
|
|
@ -46,20 +45,20 @@ func main() {
|
|||
}
|
||||
|
||||
if *website {
|
||||
err = doc.GenMarkdownTreeCustom(rootCmd, *dir, filePrepender, linkHandler)
|
||||
err = docs.GenMarkdownTreeCustom(rootCmd, *dir, filePrepender, linkHandler)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if *manPage {
|
||||
header := &doc.GenManHeader{
|
||||
header := &docs.GenManHeader{
|
||||
Title: "gh",
|
||||
Section: "1",
|
||||
Source: "", //source and manual are just put at the top of the manpage, before name
|
||||
Manual: "", //if source is an empty string, it's set to "Auto generated by spf13/cobra"
|
||||
}
|
||||
err = doc.GenManTree(rootCmd, header, *dir)
|
||||
err = docs.GenManTree(rootCmd, header, *dir)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -7,6 +7,7 @@ require (
|
|||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/briandowns/spinner v1.11.1
|
||||
github.com/charmbracelet/glamour v0.2.1-0.20200724174618-1246d13c1684
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0
|
||||
github.com/enescakir/emoji v1.0.0
|
||||
github.com/google/go-cmp v0.5.2
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
|
|
|
|||
232
internal/docs/man.go
Normal file
232
internal/docs/man.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"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, header *GenManHeader, dir string) error {
|
||||
return GenManTreeFromOpts(cmd, GenManTreeOptions{
|
||||
Header: header,
|
||||
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 {
|
||||
header := opts.Header
|
||||
if header == nil {
|
||||
header = &GenManHeader{}
|
||||
}
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
if err := GenManTreeFromOpts(c, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
section := "1"
|
||||
if header.Section != "" {
|
||||
section = header.Section
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
headerCopy := *header
|
||||
return GenMan(cmd, &headerCopy, f)
|
||||
}
|
||||
|
||||
// GenManTreeOptions is the options for generating the man pages.
|
||||
// Used only in GenManTreeFromOpts.
|
||||
type GenManTreeOptions struct {
|
||||
Header *GenManHeader
|
||||
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 and will use "Auto generated by spf13/cobra"
|
||||
// if the Source is unset.
|
||||
type GenManHeader struct {
|
||||
Title string
|
||||
Section string
|
||||
Date *time.Time
|
||||
date string
|
||||
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 header == nil {
|
||||
header = &GenManHeader{}
|
||||
}
|
||||
if err := fillHeader(header, cmd.CommandPath(), cmd.DisableAutoGenTag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := genMan(cmd, header)
|
||||
_, err := w.Write(md2man.Render(b))
|
||||
return err
|
||||
}
|
||||
|
||||
func fillHeader(header *GenManHeader, name string, disableAutoGen bool) 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
|
||||
}
|
||||
header.date = (*header.Date).Format("Jan 2006")
|
||||
if header.Source == "" && !disableAutoGen {
|
||||
header.Source = "Auto generated by spf13/cobra"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
|
||||
description := cmd.Long
|
||||
if len(description) == 0 {
|
||||
description = cmd.Short
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s"
|
||||
# NAME
|
||||
`, header.Title, header.Section, header.date, 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()))
|
||||
buf.WriteString("# DESCRIPTION\n")
|
||||
buf.WriteString(description + "\n\n")
|
||||
}
|
||||
|
||||
func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
if len(flag.Deprecated) > 0 || flag.Hidden {
|
||||
return
|
||||
}
|
||||
format := ""
|
||||
if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
|
||||
format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name)
|
||||
} else {
|
||||
format = fmt.Sprintf("**--%s**", flag.Name)
|
||||
}
|
||||
if len(flag.NoOptDefVal) > 0 {
|
||||
format += "["
|
||||
}
|
||||
if flag.Value.Type() == "string" {
|
||||
// put quotes on the value
|
||||
format += "=%q"
|
||||
} else {
|
||||
format += "=%s"
|
||||
}
|
||||
if len(flag.NoOptDefVal) > 0 {
|
||||
format += "]"
|
||||
}
|
||||
format += "\n\t%s\n\n"
|
||||
buf.WriteString(fmt.Sprintf(format, flag.DefValue, flag.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 flags.HasAvailableFlags() {
|
||||
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)
|
||||
manPrintOptions(buf, cmd)
|
||||
if len(cmd.Example) > 0 {
|
||||
buf.WriteString("# EXAMPLE\n")
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
|
||||
}
|
||||
if hasSeeAlso(cmd) {
|
||||
buf.WriteString("# SEE ALSO\n")
|
||||
seealsos := make([]string, 0)
|
||||
if cmd.HasParent() {
|
||||
parentPath := cmd.Parent().CommandPath()
|
||||
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
|
||||
seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
|
||||
seealsos = append(seealsos, seealso)
|
||||
cmd.VisitParents(func(c *cobra.Command) {
|
||||
if c.DisableAutoGenTag {
|
||||
cmd.DisableAutoGenTag = c.DisableAutoGenTag
|
||||
}
|
||||
})
|
||||
}
|
||||
children := cmd.Commands()
|
||||
sort.Sort(byName(children))
|
||||
for _, c := range children {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section)
|
||||
seealsos = append(seealsos, seealso)
|
||||
}
|
||||
buf.WriteString(strings.Join(seealsos, ", ") + "\n")
|
||||
}
|
||||
if !cmd.DisableAutoGenTag {
|
||||
buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006")))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
215
internal/docs/man_test.go
Normal file
215
internal/docs/man_test.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func translate(in string) string {
|
||||
return strings.Replace(in, "-", "\\-", -1)
|
||||
}
|
||||
|
||||
func TestGenManDoc(t *testing.T) {
|
||||
header := &GenManHeader{
|
||||
Title: "Project",
|
||||
Section: "2",
|
||||
}
|
||||
|
||||
// We generate on a subcommand so we have both subcommands and parents
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMan(echoCmd, header, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
// Make sure parent has - in CommandPath() in SEE ALSO:
|
||||
parentPath := echoCmd.Parent().CommandPath()
|
||||
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
|
||||
expected := translate(dashParentPath)
|
||||
expected = expected + "(" + header.Section + ")"
|
||||
checkStringContains(t, output, expected)
|
||||
|
||||
checkStringContains(t, output, translate(echoCmd.Name()))
|
||||
checkStringContains(t, output, translate(echoCmd.Name()))
|
||||
checkStringContains(t, output, "boolone")
|
||||
checkStringContains(t, output, "rootflag")
|
||||
checkStringContains(t, output, translate(rootCmd.Name()))
|
||||
checkStringContains(t, output, translate(echoSubCmd.Name()))
|
||||
checkStringOmits(t, output, translate(deprecatedCmd.Name()))
|
||||
checkStringContains(t, output, translate("Auto generated"))
|
||||
}
|
||||
|
||||
func TestGenManNoHiddenParents(t *testing.T) {
|
||||
header := &GenManHeader{
|
||||
Title: "Project",
|
||||
Section: "2",
|
||||
}
|
||||
|
||||
// We generate on a subcommand so we have both subcommands and parents
|
||||
for _, name := range []string{"rootflag", "strtwo"} {
|
||||
f := rootCmd.PersistentFlags().Lookup(name)
|
||||
f.Hidden = true
|
||||
defer func() { f.Hidden = false }()
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMan(echoCmd, header, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
// Make sure parent has - in CommandPath() in SEE ALSO:
|
||||
parentPath := echoCmd.Parent().CommandPath()
|
||||
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
|
||||
expected := translate(dashParentPath)
|
||||
expected = expected + "(" + header.Section + ")"
|
||||
checkStringContains(t, output, expected)
|
||||
|
||||
checkStringContains(t, output, translate(echoCmd.Name()))
|
||||
checkStringContains(t, output, translate(echoCmd.Name()))
|
||||
checkStringContains(t, output, "boolone")
|
||||
checkStringOmits(t, output, "rootflag")
|
||||
checkStringContains(t, output, translate(rootCmd.Name()))
|
||||
checkStringContains(t, output, translate(echoSubCmd.Name()))
|
||||
checkStringOmits(t, output, translate(deprecatedCmd.Name()))
|
||||
checkStringContains(t, output, translate("Auto generated"))
|
||||
checkStringOmits(t, output, "OPTIONS INHERITED FROM PARENT COMMANDS")
|
||||
}
|
||||
|
||||
func TestGenManNoGenTag(t *testing.T) {
|
||||
echoCmd.DisableAutoGenTag = true
|
||||
defer func() { echoCmd.DisableAutoGenTag = false }()
|
||||
|
||||
header := &GenManHeader{
|
||||
Title: "Project",
|
||||
Section: "2",
|
||||
}
|
||||
|
||||
// We generate on a subcommand so we have both subcommands and parents
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMan(echoCmd, header, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
unexpected := translate("#HISTORY")
|
||||
checkStringOmits(t, output, unexpected)
|
||||
unexpected = translate("Auto generated by spf13/cobra")
|
||||
checkStringOmits(t, output, unexpected)
|
||||
}
|
||||
|
||||
func TestGenManSeeAlso(t *testing.T) {
|
||||
rootCmd := &cobra.Command{Use: "root", Run: emptyRun}
|
||||
aCmd := &cobra.Command{Use: "aaa", Run: emptyRun, Hidden: true} // #229
|
||||
bCmd := &cobra.Command{Use: "bbb", Run: emptyRun}
|
||||
cCmd := &cobra.Command{Use: "ccc", Run: emptyRun}
|
||||
rootCmd.AddCommand(aCmd, bCmd, cCmd)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
header := &GenManHeader{}
|
||||
if err := GenMan(rootCmd, header, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scanner := bufio.NewScanner(buf)
|
||||
|
||||
if err := assertLineFound(scanner, ".SH SEE ALSO"); err != nil {
|
||||
t.Fatalf("Couldn't find SEE ALSO section header: %v", err)
|
||||
}
|
||||
if err := assertNextLineEquals(scanner, ".PP"); err != nil {
|
||||
t.Fatalf("First line after SEE ALSO wasn't break-indent: %v", err)
|
||||
}
|
||||
if err := assertNextLineEquals(scanner, `\fBroot\-bbb(1)\fP, \fBroot\-ccc(1)\fP`); err != nil {
|
||||
t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManPrintFlagsHidesShortDeperecated(t *testing.T) {
|
||||
c := &cobra.Command{}
|
||||
c.Flags().StringP("foo", "f", "default", "Foo flag")
|
||||
c.Flags().MarkShorthandDeprecated("foo", "don't use it no more")
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
manPrintFlags(buf, c.Flags())
|
||||
|
||||
got := buf.String()
|
||||
expected := "**--foo**=\"default\"\n\tFoo flag\n\n"
|
||||
if got != expected {
|
||||
t.Errorf("Expected %v, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenManTree(t *testing.T) {
|
||||
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
|
||||
header := &GenManHeader{Section: "2"}
|
||||
tmpdir, err := ioutil.TempDir("", "test-gen-man-tree")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tmpdir: %s", err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := GenManTree(c, header, tmpdir); err != nil {
|
||||
t.Fatalf("GenManTree failed: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(tmpdir, "do.2")); err != nil {
|
||||
t.Fatalf("Expected file 'do.2' to exist")
|
||||
}
|
||||
|
||||
if header.Title != "" {
|
||||
t.Fatalf("Expected header.Title to be unmodified")
|
||||
}
|
||||
}
|
||||
|
||||
func assertLineFound(scanner *bufio.Scanner, expectedLine string) error {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == expectedLine {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("scan failed: %s", err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("hit EOF before finding %v", expectedLine)
|
||||
}
|
||||
|
||||
func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error {
|
||||
if scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == expectedLine {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("got %v, not %v", line, expectedLine)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("scan failed: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("hit EOF before finding %v", expectedLine)
|
||||
}
|
||||
|
||||
func BenchmarkGenManToFile(b *testing.B) {
|
||||
file, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer file.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := GenMan(rootCmd, nil, file); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
142
internal/docs/markdown.go
Normal file
142
internal/docs/markdown.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
|
||||
flags := cmd.NonInheritedFlags()
|
||||
flags.SetOutput(buf)
|
||||
if flags.HasAvailableFlags() {
|
||||
buf.WriteString("### Options\n\n```\n")
|
||||
flags.PrintDefaults()
|
||||
buf.WriteString("```\n\n")
|
||||
}
|
||||
|
||||
parentFlags := cmd.InheritedFlags()
|
||||
parentFlags.SetOutput(buf)
|
||||
if parentFlags.HasAvailableFlags() {
|
||||
buf.WriteString("### Options inherited from parent commands\n\n```\n")
|
||||
parentFlags.PrintDefaults()
|
||||
buf.WriteString("```\n\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenMarkdown creates markdown output.
|
||||
func GenMarkdown(cmd *cobra.Command, w io.Writer) error {
|
||||
return GenMarkdownCustom(cmd, w, func(s string) string { return s })
|
||||
}
|
||||
|
||||
// GenMarkdownCustom creates custom markdown output.
|
||||
func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
|
||||
cmd.InitDefaultHelpCmd()
|
||||
cmd.InitDefaultHelpFlag()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
name := cmd.CommandPath()
|
||||
|
||||
buf.WriteString("## " + name + "\n\n")
|
||||
buf.WriteString(cmd.Short + "\n\n")
|
||||
if len(cmd.Long) > 0 {
|
||||
buf.WriteString("### Synopsis\n\n")
|
||||
buf.WriteString(cmd.Long + "\n\n")
|
||||
}
|
||||
|
||||
if cmd.Runnable() {
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
|
||||
}
|
||||
|
||||
if len(cmd.Example) > 0 {
|
||||
buf.WriteString("### Examples\n\n")
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
|
||||
}
|
||||
|
||||
if err := printOptions(buf, cmd, name); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasSeeAlso(cmd) {
|
||||
buf.WriteString("### SEE ALSO\n\n")
|
||||
if cmd.HasParent() {
|
||||
parent := cmd.Parent()
|
||||
pname := parent.CommandPath()
|
||||
link := pname + ".md"
|
||||
link = strings.Replace(link, " ", "_", -1)
|
||||
buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short))
|
||||
cmd.VisitParents(func(c *cobra.Command) {
|
||||
if c.DisableAutoGenTag {
|
||||
cmd.DisableAutoGenTag = c.DisableAutoGenTag
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
children := cmd.Commands()
|
||||
sort.Sort(byName(children))
|
||||
|
||||
for _, child := range children {
|
||||
if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
cname := name + " " + child.Name()
|
||||
link := cname + ".md"
|
||||
link = strings.Replace(link, " ", "_", -1)
|
||||
buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
if !cmd.DisableAutoGenTag {
|
||||
buf.WriteString("###### Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "\n")
|
||||
}
|
||||
_, err := buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
// GenMarkdownTree will generate a markdown 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 GenMarkdownTree(cmd *cobra.Command, dir string) error {
|
||||
identity := func(s string) string { return s }
|
||||
emptyStr := func(s string) string { return "" }
|
||||
return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity)
|
||||
}
|
||||
|
||||
// GenMarkdownTreeCustom is the the same as GenMarkdownTree, but
|
||||
// with custom filePrepender and linkHandler.
|
||||
func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
|
||||
filename := filepath.Join(dir, basename)
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
112
internal/docs/markdown_test.go
Normal file
112
internal/docs/markdown_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestGenMdDoc(t *testing.T) {
|
||||
// We generate on subcommand so we have both subcommands and parents.
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMarkdown(echoCmd, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
checkStringContains(t, output, echoCmd.Long)
|
||||
checkStringContains(t, output, echoCmd.Example)
|
||||
checkStringContains(t, output, "boolone")
|
||||
checkStringContains(t, output, "rootflag")
|
||||
checkStringContains(t, output, rootCmd.Short)
|
||||
checkStringContains(t, output, echoSubCmd.Short)
|
||||
checkStringOmits(t, output, deprecatedCmd.Short)
|
||||
checkStringContains(t, output, "Options inherited from parent commands")
|
||||
}
|
||||
|
||||
func TestGenMdDocWithNoLongOrSynopsis(t *testing.T) {
|
||||
// We generate on subcommand so we have both subcommands and parents.
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMarkdown(dummyCmd, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
checkStringContains(t, output, dummyCmd.Example)
|
||||
checkStringContains(t, output, dummyCmd.Short)
|
||||
checkStringContains(t, output, "Options inherited from parent commands")
|
||||
checkStringOmits(t, output, "### Synopsis")
|
||||
}
|
||||
|
||||
func TestGenMdNoHiddenParents(t *testing.T) {
|
||||
// We generate on subcommand so we have both subcommands and parents.
|
||||
for _, name := range []string{"rootflag", "strtwo"} {
|
||||
f := rootCmd.PersistentFlags().Lookup(name)
|
||||
f.Hidden = true
|
||||
defer func() { f.Hidden = false }()
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMarkdown(echoCmd, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
checkStringContains(t, output, echoCmd.Long)
|
||||
checkStringContains(t, output, echoCmd.Example)
|
||||
checkStringContains(t, output, "boolone")
|
||||
checkStringOmits(t, output, "rootflag")
|
||||
checkStringContains(t, output, rootCmd.Short)
|
||||
checkStringContains(t, output, echoSubCmd.Short)
|
||||
checkStringOmits(t, output, deprecatedCmd.Short)
|
||||
checkStringOmits(t, output, "Options inherited from parent commands")
|
||||
}
|
||||
|
||||
func TestGenMdNoTag(t *testing.T) {
|
||||
rootCmd.DisableAutoGenTag = true
|
||||
defer func() { rootCmd.DisableAutoGenTag = false }()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := GenMarkdown(rootCmd, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
checkStringOmits(t, output, "Auto generated")
|
||||
}
|
||||
|
||||
func TestGenMdTree(t *testing.T) {
|
||||
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
|
||||
tmpdir, err := ioutil.TempDir("", "test-gen-md-tree")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tmpdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := GenMarkdownTree(c, tmpdir); err != nil {
|
||||
t.Fatalf("GenMarkdownTree failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(tmpdir, "do.md")); err != nil {
|
||||
t.Fatalf("Expected file 'do.md' to exist")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenMarkdownToFile(b *testing.B) {
|
||||
file, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer file.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := GenMarkdown(rootCmd, file); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
internal/docs/utils.go
Normal file
38
internal/docs/utils.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Test to see if we have a reason to print See Also information in docs
|
||||
// Basically this is a test for a parent command or a subcommand which is
|
||||
// both not deprecated and not the autogenerated help command.
|
||||
func hasSeeAlso(cmd *cobra.Command) bool {
|
||||
if cmd.HasParent() {
|
||||
return true
|
||||
}
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Temporary workaround for yaml lib generating incorrect yaml with long strings
|
||||
// that do not contain \n.
|
||||
func forceMultiLine(s string) string {
|
||||
if len(s) > 60 && !strings.Contains(s, "\n") {
|
||||
s = s + "\n"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type byName []*cobra.Command
|
||||
|
||||
func (s byName) Len() int { return len(s) }
|
||||
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
|
||||
91
internal/docs/utils_test.go
Normal file
91
internal/docs/utils_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package docs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func emptyRun(*cobra.Command, []string) {}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringP("rootflag", "r", "two", "")
|
||||
rootCmd.PersistentFlags().StringP("strtwo", "t", "two", "help message for parent flag strtwo")
|
||||
|
||||
echoCmd.PersistentFlags().StringP("strone", "s", "one", "help message for flag strone")
|
||||
echoCmd.PersistentFlags().BoolP("persistentbool", "p", false, "help message for flag persistentbool")
|
||||
echoCmd.Flags().IntP("intone", "i", 123, "help message for flag intone")
|
||||
echoCmd.Flags().BoolP("boolone", "b", true, "help message for flag boolone")
|
||||
|
||||
timesCmd.PersistentFlags().StringP("strtwo", "t", "2", "help message for child flag strtwo")
|
||||
timesCmd.Flags().IntP("inttwo", "j", 234, "help message for flag inttwo")
|
||||
timesCmd.Flags().BoolP("booltwo", "c", false, "help message for flag booltwo")
|
||||
|
||||
printCmd.PersistentFlags().StringP("strthree", "s", "three", "help message for flag strthree")
|
||||
printCmd.Flags().IntP("intthree", "i", 345, "help message for flag intthree")
|
||||
printCmd.Flags().BoolP("boolthree", "b", true, "help message for flag boolthree")
|
||||
|
||||
echoCmd.AddCommand(timesCmd, echoSubCmd, deprecatedCmd)
|
||||
rootCmd.AddCommand(printCmd, echoCmd, dummyCmd)
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "root",
|
||||
Short: "Root short description",
|
||||
Long: "Root long description",
|
||||
Run: emptyRun,
|
||||
}
|
||||
|
||||
var echoCmd = &cobra.Command{
|
||||
Use: "echo [string to echo]",
|
||||
Aliases: []string{"say"},
|
||||
Short: "Echo anything to the screen",
|
||||
Long: "an utterly useless command for testing",
|
||||
Example: "Just run cobra-test echo",
|
||||
}
|
||||
|
||||
var echoSubCmd = &cobra.Command{
|
||||
Use: "echosub [string to print]",
|
||||
Short: "second sub command for echo",
|
||||
Long: "an absolutely utterly useless command for testing gendocs!.",
|
||||
Run: emptyRun,
|
||||
}
|
||||
|
||||
var timesCmd = &cobra.Command{
|
||||
Use: "times [# times] [string to echo]",
|
||||
SuggestFor: []string{"counts"},
|
||||
Short: "Echo anything to the screen more times",
|
||||
Long: `a slightly useless command for testing.`,
|
||||
Run: emptyRun,
|
||||
}
|
||||
|
||||
var deprecatedCmd = &cobra.Command{
|
||||
Use: "deprecated [can't do anything here]",
|
||||
Short: "A command which is deprecated",
|
||||
Long: `an absolutely utterly useless command for testing deprecation!.`,
|
||||
Deprecated: "Please use echo instead",
|
||||
}
|
||||
|
||||
var printCmd = &cobra.Command{
|
||||
Use: "print [string to print]",
|
||||
Short: "Print anything to the screen",
|
||||
Long: `an absolutely utterly useless command for testing.`,
|
||||
}
|
||||
|
||||
var dummyCmd = &cobra.Command{
|
||||
Use: "dummy [action]",
|
||||
Short: "Performs a dummy action",
|
||||
}
|
||||
|
||||
func checkStringContains(t *testing.T, got, expected string) {
|
||||
if !strings.Contains(got, expected) {
|
||||
t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func checkStringOmits(t *testing.T, got, expected string) {
|
||||
if strings.Contains(got, expected) {
|
||||
t.Errorf("Expected to not contain: \n %v\nGot: %v", expected, got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue