Repository: spf13/cobra
Pull request #145 (2015-09-08 22:53:30)

Enhance Man Page Generation
This actually breaks API, but since the old API just flat didn't get it right and there are no known users yet, I don't feel too badly...


Patch:
diff --git a/man_docs.go b/man_docs.go
index f260990..e5edbfd 100644
--- a/man_docs.go
+++ b/man_docs.go
@@ -25,20 +25,29 @@ import (
 	"github.com/spf13/pflag"
 )
 
-func GenManTree(cmd *Command, projectName, dir string) {
-	cmd.GenManTree(projectName, dir)
+// GenManTree will call cmd.GenManTree(header, dir)
+func GenManTree(cmd *Command, header *GenManHeader, dir string) {
+	cmd.GenManTree(header, dir)
 }
 
-func (cmd *Command) GenManTree(projectName, dir string) {
+// GenManTree will generate a man page for this command and all decendants
+// 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 (cmd *Command) GenManTree(header *GenManHeader, dir string) {
+	if header == nil {
+		header = &GenManHeader{}
+	}
 	for _, c := range cmd.Commands() {
 		if len(c.Deprecated) != 0 || c == cmd.helpCommand {
 			continue
 		}
-		GenManTree(c, projectName, dir)
+		GenManTree(c, header, dir)
 	}
 	out := new(bytes.Buffer)
 
-	cmd.GenMan(projectName, out)
+	cmd.GenMan(header, out)
 
 	filename := cmd.CommandPath()
 	filename = dir + strings.Replace(filename, " ", "-", -1) + ".1"
@@ -55,21 +64,58 @@ func (cmd *Command) GenManTree(projectName, dir string) {
 	}
 }
 
-func GenMan(cmd *Command, projectName string, out *bytes.Buffer) {
-	cmd.GenMan(projectName, out)
+// 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 if 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
 }
 
-func (cmd *Command) GenMan(projectName string, out *bytes.Buffer) {
+// GenMan will call cmd.GenMan(header, out)
+func GenMan(cmd *Command, header *GenManHeader, out *bytes.Buffer) {
+	cmd.GenMan(header, out)
+}
 
-	buf := genMarkdown(cmd, projectName)
+// GenMan will generate a man page for the given command in the out buffer.
+// The header argument may be nil, however obviously out may not.
+func (cmd *Command) GenMan(header *GenManHeader, out *bytes.Buffer) {
+	if header == nil {
+		header = &GenManHeader{}
+	}
+	buf := genMarkdown(cmd, header)
 	final := mangen.Render(buf)
 	out.Write(final)
 }
 
-func manPreamble(out *bytes.Buffer, projectName, name, short, long string) {
-	fmt.Fprintf(out, `%% %s(1)
+func fillHeader(header *GenManHeader, name string) {
+	if header.Title == "" {
+		header.Title = name
+	}
+	if header.Section == "" {
+		header.Section = "1"
+	}
+	if header.Date == nil {
+		now := time.Now()
+		header.Date = &now
+	}
+	header.date = (*header.Date).Format("Jan 2006")
+	if header.Source == "" {
+		header.Source = "Auto generated by spf13/cobra"
+	}
+}
+
+func manPreamble(out *bytes.Buffer, header *GenManHeader, name, short, long string) {
+	fmt.Fprintf(out, `%% %s(%s)%s
+%% %s
+%% %s
 # NAME
-`, projectName)
+`, header.Title, header.Section, header.date, header.Source, header.Manual)
 	fmt.Fprintf(out, "%s \\- %s\n\n", name, short)
 	fmt.Fprintf(out, "# SYNOPSIS\n")
 	fmt.Fprintf(out, "**%s** [OPTIONS]\n\n", name)
@@ -120,7 +166,8 @@ func manPrintOptions(out *bytes.Buffer, command *Command) {
 	}
 }
 
-func genMarkdown(cmd *Command, projectName string) []byte {
+func genMarkdown(cmd *Command, header *GenManHeader) []byte {
+	fillHeader(header, cmd.Name())
 	// something like `rootcmd subcmd1 subcmd2`
 	commandName := cmd.CommandPath()
 	// something like `rootcmd-subcmd1-subcmd2`
@@ -134,7 +181,7 @@ func genMarkdown(cmd *Command, projectName string) []byte {
 		long = short
 	}
 
-	manPreamble(buf, projectName, commandName, short, long)
+	manPreamble(buf, header, commandName, short, long)
 	manPrintOptions(buf, cmd)
 
 	if len(cmd.Example) > 0 {
@@ -145,7 +192,9 @@ func genMarkdown(cmd *Command, projectName string) []byte {
 	if cmd.hasSeeAlso() {
 		fmt.Fprintf(buf, "# SEE ALSO\n")
 		if cmd.HasParent() {
-			fmt.Fprintf(buf, "**%s(1)**, ", cmd.Parent().CommandPath())
+			parentPath := cmd.Parent().CommandPath()
+			dashParentPath := strings.Replace(parentPath, " ", "-", -1)
+			fmt.Fprintf(buf, "**%s(%s)**, ", dashParentPath, header.Section)
 		}
 
 		children := cmd.Commands()
@@ -154,11 +203,11 @@ func genMarkdown(cmd *Command, projectName string) []byte {
 			if len(c.Deprecated) != 0 || c == cmd.helpCommand {
 				continue
 			}
-			fmt.Fprintf(buf, "**%s-%s(1)**, ", dashCommandName, c.Name())
+			fmt.Fprintf(buf, "**%s-%s(%s)**, ", dashCommandName, c.Name(), header.Section)
 		}
 		fmt.Fprintf(buf, "\n")
 	}
 
-	fmt.Fprintf(buf, "# HISTORY\n%s Auto generated by spf13/cobra\n", time.Now().UTC())
+	fmt.Fprintf(buf, "# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006"))
 	return buf.Bytes()
 }
diff --git a/man_docs.md b/man_docs.md
index 3516d37..a764099 100644
--- a/man_docs.md
+++ b/man_docs.md
@@ -14,7 +14,11 @@ func main() {
 		Use:   "test",
 		Short: "my test program",
 	}
-	cmd.GenManTree("/tmp")
+	header := &cobra.GenManHeader{
+		Title: "MINE",
+		Section: "3",
+	}
+	cmd.GenManTree(header, "/tmp")
 }
 ```
 
diff --git a/md_docs.go b/md_docs.go
index dde5b11..4e5b23a 100644
--- a/md_docs.go
+++ b/md_docs.go
@@ -108,7 +108,7 @@ func (cmd *Command) GenMarkdownCustom(out *bytes.Buffer, linkHandler func(string
 		fmt.Fprintf(out, "\n")
 	}
 
-	fmt.Fprintf(out, "###### Auto generated by spf13/cobra at %s\n", time.Now().UTC())
+	fmt.Fprintf(out, "###### Auto generated by spf13/cobra on %s\n", time.Now().Format("2-Jan-2006"))
 }
 
 func GenMarkdownTree(cmd *Command, dir string) {


Test patch:
diff --git a/cobra_test.go b/cobra_test.go
index 3aed7dd..0163269 100644
--- a/cobra_test.go
+++ b/cobra_test.go
@@ -255,16 +255,24 @@ func logErr(t *testing.T, found, expected string) {
 	t.Errorf(out.String())
 }
 
+func checkStringContains(t *testing.T, found, expected string) {
+	if !strings.Contains(found, expected) {
+		logErr(t, found, expected)
+	}
+}
+
 func checkResultContains(t *testing.T, x resulter, check string) {
-	if !strings.Contains(x.Output, check) {
-		logErr(t, x.Output, check)
+	checkStringContains(t, x.Output, check)
+}
+
+func checkStringOmits(t *testing.T, found, expected string) {
+	if strings.Contains(found, expected) {
+		logErr(t, found, expected)
 	}
 }
 
 func checkResultOmits(t *testing.T, x resulter, check string) {
-	if strings.Contains(x.Output, check) {
-		logErr(t, x.Output, check)
-	}
+	checkStringOmits(t, x.Output, check)
 }
 
 func checkOutputContains(t *testing.T, c *Command, check string) {
@@ -976,15 +984,15 @@ func TestFlagOnPflagCommandLine(t *testing.T) {
 func TestAddTemplateFunctions(t *testing.T) {
 	AddTemplateFunc("t", func() bool { return true })
 	AddTemplateFuncs(template.FuncMap{
-		"f": func() bool { return false }, 
-		"h": func() string { return "Hello," }, 
+		"f": func() bool { return false },
+		"h": func() string { return "Hello," },
 		"w": func() string { return "world." }})
 
 	const usage = "Hello, world."
-	
+
 	c := &Command{}
 	c.SetUsageTemplate(`{{if t}}{{h}}{{end}}{{if f}}{{h}}{{end}} {{w}}`)
-	
+
 	if us := c.UsageString(); us != usage {
 		t.Errorf("c.UsageString() != \"%s\", is \"%s\"", usage, us)
 	}
diff --git a/examples_test.go b/examples_test.go
new file mode 100644
index 0000000..10da985
--- /dev/null
+++ b/examples_test.go
@@ -0,0 +1,34 @@
+package cobra_test
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+func ExampleCommand_GenManTree() {
+	cmd := &cobra.Command{
+		Use:   "test",
+		Short: "my test program",
+	}
+	header := &cobra.GenManHeader{
+		Title:   "MINE",
+		Section: "3",
+	}
+	cmd.GenManTree(header, "/tmp")
+}
+
+func ExampleCommand_GenMan() {
+	cmd := &cobra.Command{
+		Use:   "test",
+		Short: "my test program",
+	}
+	header := &cobra.GenManHeader{
+		Title:   "MINE",
+		Section: "3",
+	}
+	out := new(bytes.Buffer)
+	cmd.GenMan(header, out)
+	fmt.Print(out.String())
+}
diff --git a/man_docs_test.go b/man_docs_test.go
index 6e31ab3..1c121d6 100644
--- a/man_docs_test.go
+++ b/man_docs_test.go
@@ -24,48 +24,45 @@ func TestGenManDoc(t *testing.T) {
 
 	out := new(bytes.Buffer)
 
+	header := &GenManHeader{
+		Title:   "Project",
+		Section: "2",
+	}
 	// We generate on a subcommand so we have both subcommands and parents
-	cmdEcho.GenMan("PROJECT", out)
+	cmdEcho.GenMan(header, out)
 	found := out.String()
 
+	// Make sure parent has - in CommandPath() in SEE ALSO:
+	parentPath := cmdEcho.Parent().CommandPath()
+	dashParentPath := strings.Replace(parentPath, " ", "-", -1)
+	expected := translate(dashParentPath)
+	expected = expected + "(" + header.Section + ")"
+	checkStringContains(t, found, expected)
+
 	// Our description
-	expected := translate(cmdEcho.Name())
-	if !strings.Contains(found, expected) {
-		t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
-	}
+	expected = translate(cmdEcho.Name())
+	checkStringContains(t, found, expected)
 
 	// Better have our example
 	expected = translate(cmdEcho.Name())
-	if !strings.Contains(found, expected) {
-		t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
-	}
+	checkStringContains(t, found, expected)
 
 	// A local flag
 	expected = "boolone"
-	if !strings.Contains(found, expected) {
-		t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
-	}
+	checkStringContains(t, found, expected)
 
 	// persistent flag on parent
 	expected = "rootflag"
-	if !strings.Contains(found, expected) {
-		t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
-	}
+	checkStringContains(t, found, expected)
 
 	// We better output info about our parent
 	expected = translate(cmdRootWithRun.Name())
-	if !strings.Contains(found, expected) {
-		t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
-	}
+	checkStringContains(t, found, expected)
 
 	// And about subcommands
 	expected = translate(cmdEchoSub.Name())
-	if !strings.Contains(found, expected) {
-		t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
-	}
+	checkStringContains(t, found, expected)
 
 	unexpected := translate(cmdDeprecated.Name())
-	if strings.Contains(found, unexpected) {
-		t.Errorf("Unexpected response.\nFound: %v\nBut should not have!!\n", unexpected)
-	}
+	checkStringOmits(t, found, unexpected)
 }