Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added asciidoc generation and associated tests. #1698

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Documentation generation

- [Asciidoc docs](./asciidoc_docs.md)
- [Man page docs](./man_docs.md)
- [Markdown docs](./md_docs.md)
- [Rest docs](./rest_docs.md)
Expand Down
172 changes: 172 additions & 0 deletions doc/asciidoc_docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package doc

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"

"github.com/spf13/cobra"
)

func printOptionsAsciidoc(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("== Options\n")
buf.WriteString("\n")
buf.WriteString(".Options\n")
buf.WriteString("[source,text]\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")
buf.WriteString(".Parent Options\n")
buf.WriteString("[source,text]\n----\n")
parentFlags.PrintDefaults()
buf.WriteString("----\n\n")
}
return nil
}

// GenAsciidoc creates Asciidoc output.
func GenAsciidoc(cmd *cobra.Command, w io.Writer) error {
return GenAsciidocCustom(cmd, w, func(s string) string { return s })
}

// GenAsciidocCustom creates custom Asciidoc output.
func GenAsciidocCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()

buf := new(bytes.Buffer)
name := cmd.CommandPath()
splitName := strings.Split(cmd.CommandPath(), " ")

if len(splitName) < 2 {
buf.WriteString("= " + name + "\n\n")
} else {
buf.WriteString("= " + splitName[0] + ":")
for index, cmdName := range splitName {
if index < 1 {
// Skip the root name.
continue
}
buf.WriteString(" " + cmdName)
}
buf.WriteString("\n\n")
}
// No point in printing the description if it's empty.
if len(cmd.Short) > 0 {
buf.WriteString("[.lead]\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("[source,text]\n"))
buf.WriteString(fmt.Sprintf("----\n%s\n----\n\n", cmd.UseLine()))
}

if len(cmd.Example) > 0 {
buf.WriteString("== Examples\n\n")
buf.WriteString(".Examples Options\n")
buf.WriteString("[source,text]\n")
buf.WriteString(fmt.Sprintf("----\n%s\n----\n\n", cmd.Example))
}

if err := printOptionsAsciidoc(buf, cmd, name); err != nil {
return err
}
if hasSeeAlso(cmd) {
buf.WriteString(fmt.Sprintf("== SEE ALSO\n\n"))
if cmd.HasParent() {
parent := cmd.Parent()
pname := parent.CommandPath()
link := pname + ".adoc"
link = strings.ReplaceAll(link, " ", "_")
buf.WriteString(fmt.Sprintf("* xref:%s[%s]\t - %s\n", linkHandler(link), pname, 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 + ".adoc"
link = strings.ReplaceAll(link, " ", "_")
buf.WriteString(fmt.Sprintf("* xref:%s[%s]\t - %s\n", linkHandler(link), cname, child.Short))
}
buf.WriteString("\n")
}
if !cmd.DisableAutoGenTag {

buf.WriteString("\n.Last Generated\n")
buf.WriteString("****\n")
buf.WriteString("Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "\n")
buf.WriteString("****\n")
}
_, err := buf.WriteTo(w)
return err
}

// GenAsciidocTree will generate a Asciidoc 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 GenAsciidocTree(cmd *cobra.Command, dir string) error {
identity := func(s string) string { return s }
emptyStr := func(s string) string { return "" }
return GenAsciidocTreeCustom(cmd, dir, emptyStr, identity)
}

// GenAsciidocTreeCustom is the the same as GenAsciidocTree, but
// with custom filePrepender and linkHandler.
func GenAsciidocTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
if err := GenAsciidocTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
return err
}
}

basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".adoc"
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 := GenAsciidocCustom(cmd, f, linkHandler); err != nil {
return err
}
return nil
}
115 changes: 115 additions & 0 deletions doc/asciidoc_docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Generating Asciidoc Docs For Your Own cobra.Command

Generating Asciidoc pages from a cobra command is incredibly easy. An example is as follows:

```go
package main

import (
"log"

"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)

func main() {
cmd := &cobra.Command{
Use: "test",
Short: "my test program",
}
err := doc.GenAsciidocTree(cmd, "/tmp")
if err != nil {
log.Fatal(err)
}
}
```

That will get you a Asciidoc document `/tmp/test.adoc`

## Generate Asciidoc docs for the entire command tree

This program could generate docs for the kubectl command in the kubernetes project

```go
package main

import (
"log"
"io/ioutil"
"os"

"k8s.io/kubernetes/pkg/kubectl/cmd"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"

"github.com/spf13/cobra/doc"
)

func main() {
kubectl := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, ioutil.Discard, ioutil.Discard)
err := doc.GenAsciidocTree(kubectl, "./")
if err != nil {
log.Fatal(err)
}
}
```

This will generate a whole series of files, one for each command in the tree, in the directory specified (in this case "./")

## Generate Asciidoc docs for a single command

You may wish to have more control over the output, or only generate for a single command, instead of the entire command tree. If this is the case you may prefer to `GenAsciidoc` instead of `GenAsciidocTree`

```go
out := new(bytes.Buffer)
err := doc.GenAsciidoc(cmd, out)
if err != nil {
log.Fatal(err)
}
```

This will write the Asciidoc doc for ONLY "cmd" into the out, buffer.

## Customize the output

Both `GenAsciidoc` and `GenAsciidocTree` have alternate versions with callbacks to get some control of the output:

```go
func GenAsciidocTreeCustom(cmd *Command, dir string, filePrepender, linkHandler func(string) string) error {
//...
}
```

```go
func GenAsciidocCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) string) error {
//...
}
```

The `filePrepender` will prepend the return value given the full filepath to the rendered Asciidoc file. A common use case is to add front matter to use the generated documentation with [Hugo](http://gohugo.io/):

```go
const fmTemplate = `---
date: %s
title: "%s"
slug: %s
url: %s
---
`

filePrepender := func(filename string) string {
now := time.Now().Format(time.RFC3339)
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(fmTemplate, now, strings.Replace(base, "_", " ", -1), base, url)
}
```

The `linkHandler` can be used to customize the rendered internal links to the commands, given a filename:

```go
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/commands/" + strings.ToLower(base) + "/"
}
```
Loading