From 9e7788657f7d4cdc20007a16ba586b4ba0c3aab1 Mon Sep 17 00:00:00 2001 From: Eric Paris Date: Tue, 18 Aug 2015 15:33:41 -0700 Subject: [PATCH] Auto generation of a man page --- README.md | 4 ++ bash_completions.go | 4 +- doc_util.go | 36 ++++++++++ man_docs.go | 166 ++++++++++++++++++++++++++++++++++++++++++++ man_docs.md | 21 ++++++ man_docs_test.go | 71 +++++++++++++++++++ md_docs.go | 31 +++++++-- 7 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 doc_util.go create mode 100644 man_docs.go create mode 100644 man_docs.md create mode 100644 man_docs_test.go diff --git a/README.md b/README.md index b1fb0889..5b97e021 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,10 @@ func main() { Cobra can generate a markdown formatted document based on the subcommands, flags, etc. A simple example of how to do this for your command can be found in [Markdown Docs](md_docs.md) +## Generating man pages for your command + +Cobra can generate a man page based on the subcommands, flags, etc. A simple example of how to do this for your command can be found in [Man Docs](man_docs.md) + ## Generating bash completions for your command Cobra can generate a bash completions file. If you add more information to your command these completions can be amazingly powerful and flexible. Read more about [Bash Completions](bash_completions.md) diff --git a/bash_completions.go b/bash_completions.go index 82c4274a..bc60765b 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -212,7 +212,7 @@ func postscript(out *bytes.Buffer, name string) { func writeCommands(cmd *Command, out *bytes.Buffer) { fmt.Fprintf(out, " commands=()\n") for _, c := range cmd.Commands() { - if len(c.Deprecated) > 0 { + if len(c.Deprecated) > 0 || c == cmd.helpCommand { continue } fmt.Fprintf(out, " commands+=(%q)\n", c.Name()) @@ -321,7 +321,7 @@ func writeRequiredNoun(cmd *Command, out *bytes.Buffer) { func gen(cmd *Command, out *bytes.Buffer) { for _, c := range cmd.Commands() { - if len(c.Deprecated) > 0 { + if len(c.Deprecated) > 0 || c == cmd.helpCommand { continue } gen(c, out) diff --git a/doc_util.go b/doc_util.go new file mode 100644 index 00000000..9c20bca8 --- /dev/null +++ b/doc_util.go @@ -0,0 +1,36 @@ +// Copyright 2015 Red Hat Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import () + +// Test to see if we have a reason to print See Also information in docs +// Basically this is a test for a parent commend or a subcommand which is +// both not deprecated and not the autogenerated help command. +func (cmd *Command) hasSeeAlso() bool { + if cmd.HasParent() { + return true + } + children := cmd.Commands() + if len(children) == 0 { + return false + } + for _, c := range children { + if len(c.Deprecated) != 0 || c == cmd.helpCommand { + continue + } + return true + } + return false +} diff --git a/man_docs.go b/man_docs.go new file mode 100644 index 00000000..587ad9dc --- /dev/null +++ b/man_docs.go @@ -0,0 +1,166 @@ +// Copyright 2015 Red Hat Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "fmt" + "os" + "sort" + "strings" + "time" + + mangen "github.com/cpuguy83/go-md2man/md2man" + "github.com/spf13/pflag" +) + +func GenManTree(cmd *Command, projectName, dir string) { + cmd.GenManTree(projectName, dir) +} + +func (cmd *Command) GenManTree(projectName, dir string) { + for _, c := range cmd.Commands() { + if len(c.Deprecated) != 0 || c == cmd.helpCommand { + continue + } + GenManTree(c, projectName, dir) + } + out := new(bytes.Buffer) + + cmd.GenMan(projectName, out) + + filename := cmd.CommandPath() + filename = dir + strings.Replace(filename, " ", "-", -1) + ".1" + outFile, err := os.Create(filename) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer outFile.Close() + _, err = outFile.Write(out.Bytes()) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func GenMan(cmd *Command, projectName string, out *bytes.Buffer) { + cmd.GenMan(projectName, out) +} + +func (cmd *Command) GenMan(projectName string, out *bytes.Buffer) { + + buf := genMarkdown(cmd, projectName) + final := mangen.Render(buf) + out.Write(final) +} + +func manPreamble(out *bytes.Buffer, projectName, name, short, long string) { + fmt.Fprintf(out, `%% %s(1) kubernetes User Manuals +%% Eric Paris +%% Jan 2015 +# NAME +`, projectName) + fmt.Fprintf(out, "%s \\- %s\n\n", name, short) + fmt.Fprintf(out, "# SYNOPSIS\n") + fmt.Fprintf(out, "**%s** [OPTIONS]\n\n", name) + fmt.Fprintf(out, "# DESCRIPTION\n") + fmt.Fprintf(out, "%s\n\n", long) +} + +func manPrintFlags(out *bytes.Buffer, flags *pflag.FlagSet) { + flags.VisitAll(func(flag *pflag.Flag) { + if len(flag.Deprecated) > 0 { + return + } + format := "" + if len(flag.Shorthand) > 0 { + format = "**-%s**, **--%s**" + } else { + format = "%s**--%s**" + } + if len(flag.NoOptDefVal) > 0 { + format = format + "[" + } + if flag.Value.Type() == "string" { + // put quotes on the value + format = format + "=%q" + } else { + format = format + "=%s" + } + if len(flag.NoOptDefVal) > 0 { + format = format + "]" + } + format = format + "\n\t%s\n\n" + fmt.Fprintf(out, format, flag.Shorthand, flag.Name, flag.DefValue, flag.Usage) + }) +} + +func manPrintOptions(out *bytes.Buffer, command *Command) { + flags := command.NonInheritedFlags() + if flags.HasFlags() { + fmt.Fprintf(out, "# OPTIONS\n") + manPrintFlags(out, flags) + fmt.Fprintf(out, "\n") + } + flags = command.InheritedFlags() + if flags.HasFlags() { + fmt.Fprintf(out, "# OPTIONS INHERITED FROM PARENT COMMANDS\n") + manPrintFlags(out, flags) + fmt.Fprintf(out, "\n") + } +} + +func genMarkdown(cmd *Command, projectName string) []byte { + // something like `rootcmd subcmd1 subcmd2` + commandName := cmd.CommandPath() + // something like `rootcmd-subcmd1-subcmd2` + dashCommandName := strings.Replace(commandName, " ", "-", -1) + + buf := new(bytes.Buffer) + + short := cmd.Short + long := cmd.Long + if len(long) == 0 { + long = short + } + + manPreamble(buf, projectName, commandName, short, long) + manPrintOptions(buf, cmd) + + if len(cmd.Example) > 0 { + fmt.Fprintf(buf, "# EXAMPLE\n") + fmt.Fprintf(buf, "```\n%s\n```\n", cmd.Example) + } + + if cmd.hasSeeAlso() { + fmt.Fprintf(buf, "# SEE ALSO\n") + if cmd.HasParent() { + fmt.Fprintf(buf, "**%s(1)**, ", cmd.Parent().CommandPath()) + } + + children := cmd.Commands() + sort.Sort(byName(children)) + for _, c := range children { + if len(c.Deprecated) != 0 || c == cmd.helpCommand { + continue + } + fmt.Fprintf(buf, "**%s-%s(1)**, ", dashCommandName, c.Name()) + } + fmt.Fprintf(buf, "\n") + } + + fmt.Fprintf(buf, "# HISTORY\n%s Auto generated by spf13/cobra\n", time.Now().UTC()) + return buf.Bytes() +} diff --git a/man_docs.md b/man_docs.md new file mode 100644 index 00000000..3516d37c --- /dev/null +++ b/man_docs.md @@ -0,0 +1,21 @@ +# Generating Man Pages For Your Own cobra.Command + +Generating bash completions from a cobra command is incredibly easy. An example is as follows: + +```go +package main + +import ( + "github.com/spf13/cobra" +) + +func main() { + cmd := &cobra.Command{ + Use: "test", + Short: "my test program", + } + cmd.GenManTree("/tmp") +} +``` + +That will get you a man page `/tmp/test.1` diff --git a/man_docs_test.go b/man_docs_test.go new file mode 100644 index 00000000..6e31ab3b --- /dev/null +++ b/man_docs_test.go @@ -0,0 +1,71 @@ +package cobra + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" +) + +var _ = fmt.Println +var _ = os.Stderr + +func translate(in string) string { + return strings.Replace(in, "-", "\\-", -1) +} + +func TestGenManDoc(t *testing.T) { + c := initializeWithRootCmd() + // Need two commands to run the command alphabetical sort + cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated) + c.AddCommand(cmdPrint, cmdEcho) + cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp) + + out := new(bytes.Buffer) + + // We generate on a subcommand so we have both subcommands and parents + cmdEcho.GenMan("PROJECT", out) + found := out.String() + + // 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) + } + + // 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) + } + + // 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) + } + + // 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) + } + + // 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) + } + + // 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) + } + + unexpected := translate(cmdDeprecated.Name()) + if strings.Contains(found, unexpected) { + t.Errorf("Unexpected response.\nFound: %v\nBut should not have!!\n", unexpected) + } +} diff --git a/md_docs.go b/md_docs.go index 6092c85a..dde5b114 100644 --- a/md_docs.go +++ b/md_docs.go @@ -47,10 +47,18 @@ 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() } func GenMarkdown(cmd *Command, out *bytes.Buffer) { - GenMarkdownCustom(cmd, out, func(s string) string { return s }) + cmd.GenMarkdown(out) +} + +func (cmd *Command) GenMarkdown(out *bytes.Buffer) { + cmd.GenMarkdownCustom(out, func(s string) string { return s }) } func GenMarkdownCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) string) { + cmd.GenMarkdownCustom(out, linkHandler) +} + +func (cmd *Command) GenMarkdownCustom(out *bytes.Buffer, linkHandler func(string) string) { name := cmd.CommandPath() short := cmd.Short @@ -75,7 +83,7 @@ func GenMarkdownCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) printOptions(out, cmd, name) - if len(cmd.Commands()) > 0 || cmd.HasParent() { + if cmd.hasSeeAlso() { fmt.Fprintf(out, "### SEE ALSO\n") if cmd.HasParent() { parent := cmd.Parent() @@ -89,7 +97,7 @@ func GenMarkdownCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) sort.Sort(byName(children)) for _, child := range children { - if len(child.Deprecated) > 0 { + if len(child.Deprecated) > 0 || child == cmd.helpCommand { continue } cname := name + " " + child.Name() @@ -104,18 +112,29 @@ func GenMarkdownCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) } func GenMarkdownTree(cmd *Command, dir string) { + cmd.GenMarkdownTree(dir) +} + +func (cmd *Command) GenMarkdownTree(dir string) { identity := func(s string) string { return s } emptyStr := func(s string) string { return "" } - GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) + cmd.GenMarkdownTreeCustom(dir, emptyStr, identity) } func GenMarkdownTreeCustom(cmd *Command, dir string, filePrepender func(string) string, linkHandler func(string) string) { + cmd.GenMarkdownTreeCustom(dir, filePrepender, linkHandler) +} + +func (cmd *Command) GenMarkdownTreeCustom(dir string, filePrepender func(string) string, linkHandler func(string) string) { for _, c := range cmd.Commands() { - GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler) + if len(c.Deprecated) != 0 || c == cmd.helpCommand { + continue + } + c.GenMarkdownTreeCustom(dir, filePrepender, linkHandler) } out := new(bytes.Buffer) - GenMarkdownCustom(cmd, out, linkHandler) + cmd.GenMarkdownCustom(out, linkHandler) filename := cmd.CommandPath() filename = dir + strings.Replace(filename, " ", "_", -1) + ".md"