Skip to content

Commit

Permalink
Add markdown and man page docs generation methods
Browse files Browse the repository at this point in the history
This adds two new methods to the `App` struct:

- `ToMarkdown`: creates a markdown documentation string
- `ToMan`: creates a man page string

Signed-off-by: Sascha Grunert <mail@saschagrunert.de>
  • Loading branch information
saschagrunert committed Aug 3, 2019
1 parent 07c1638 commit 641aae9
Show file tree
Hide file tree
Showing 2 changed files with 371 additions and 0 deletions.
289 changes: 289 additions & 0 deletions docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
package cli

import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"text/template"
"time"

"github.com/cpuguy83/go-md2man/md2man"
)

const markdownTemplateString = `% {{ .App.Name }}(8) {{ .App.Description }}
% {{ .App.Author }}
% {{ .Date }}
# NAME
{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }}
# SYNOPSIS
{{ .App.Name }}
{{ if .SynopsisArgs }}
` + "```" + `{{ range $v := .SynopsisArgs }}
{{ $v }}{{ end }}
` + "```" + `
{{ end }}{{ if .App.UsageText }}
# DESCRIPTION
{{ .App.UsageText }}
{{ end }}
**Usage**:
` + "```" + `
{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
` + "```" + `
{{ if .GlobalArgs }}
# GLOBAL OPTIONS
{{ range $v := .GlobalArgs }}
{{ $v }}{{ end }}
{{ end }}{{ if .Commands }}
# COMMANDS
{{ range $v := .Commands }}
{{ $v }}{{ end }}{{ end }}`

// ToMarkdown creates a markdown string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToMarkdown() (string, error) {
var w bytes.Buffer
if err := a.write(&w); err != nil {
return "", err
}
return w.String(), nil
}

// ToMan creates a man page string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToMan() (string, error) {
var w bytes.Buffer
if err := a.write(&w); err != nil {
return "", err
}
man := md2man.Render(w.Bytes())
return string(man), nil
}

type CliTemplate struct {
App *App
Date string
Commands []string
GlobalArgs []string
SynopsisArgs []string
}

func (a *App) write(w io.Writer) error {
now := time.Now()
const name = "cli"
t, err := template.New(name).Parse(markdownTemplateString)
if err != nil {
return err
}
return t.ExecuteTemplate(w, name, &CliTemplate{
App: a,
Date: fmt.Sprintf("%s %d", now.Month(), now.Year()),
Commands: prepareCommands(a.Commands, 0),
GlobalArgs: prepareArgsWithValues(a.Flags),
SynopsisArgs: prepareArgsSynopsis(a.Flags),
})
}

const nl = "\n"
const noDescription = "_no description available_"

func prepareCommands(commands []Command, level int) []string {
coms := []string{}
for i := range commands {
command := &commands[i]
prepared := strings.Repeat("#", level+2) + " " +
strings.Join(command.Names(), ", ") + nl

usage := noDescription
if command.Usage != "" {
usage = command.Usage
}
prepared += nl + usage + nl

flags := prepareArgsWithValues(command.Flags)
if len(flags) > 0 {
prepared += nl
}
prepared += strings.Join(flags, nl)
if len(flags) > 0 {
prepared += nl
}

coms = append(coms, prepared)

// recursevly iterate subcommands
if len(command.Subcommands) > 0 {
coms = append(
coms,
prepareCommands(command.Subcommands, level+1)...,
)
}
}

return coms
}

func prepareArgsWithValues(flags []Flag) []string {
return prepareFlags(flags, ", ", "**", "**", `""`, true)
}

func prepareArgsSynopsis(flags []Flag) []string {
return prepareFlags(flags, "|", "[", "]", "[value]", false)
}

func prepareFlags(
flags []Flag,
sep, opener, closer, value string,
addDetails bool,
) []string {
args := []string{}
for _, flag := range flags {
modifiedArg := opener
for _, s := range strings.Split(flag.GetName(), ",") {
trimmed := strings.TrimSpace(s)
if len(modifiedArg) > len(opener) {
modifiedArg += sep
}
if len(trimmed) > 1 {
modifiedArg += "--" + trimmed
} else {
modifiedArg += "-" + trimmed
}
}
modifiedArg += closer
if flagTakesValue(flag) {
modifiedArg += "=" + value
}

if addDetails {
modifiedArg += flagDetails(flag)
}

args = append(args, modifiedArg)

}
sort.Strings(args)
return args
}

// flagTakesValue returns true if the flag takes a value, otherwise false
func flagTakesValue(flag Flag) bool {
if _, ok := flag.(BoolFlag); ok {
return false
}
if _, ok := flag.(BoolTFlag); ok {
return false
}
if _, ok := flag.(DurationFlag); ok {
return true
}
if _, ok := flag.(Float64Flag); ok {
return true
}
if _, ok := flag.(GenericFlag); ok {
return true
}
if _, ok := flag.(Int64Flag); ok {
return true
}
if _, ok := flag.(IntFlag); ok {
return true
}
if _, ok := flag.(IntSliceFlag); ok {
return true
}
if _, ok := flag.(Int64SliceFlag); ok {
return true
}
if _, ok := flag.(StringFlag); ok {
return true
}
if _, ok := flag.(StringSliceFlag); ok {
return true
}
if _, ok := flag.(Uint64Flag); ok {
return true
}
if _, ok := flag.(UintFlag); ok {
return true
}
return false
}

// flagDetails returns a string containing the flags metadata
func flagDetails(flag Flag) string {
description := ""
value := ""
if f, ok := flag.(BoolFlag); ok {
description = f.Usage
}
if f, ok := flag.(BoolTFlag); ok {
description = f.Usage
}
if f, ok := flag.(DurationFlag); ok {
description = f.Usage
value = f.Value.String()
}
if f, ok := flag.(Float64Flag); ok {
description = f.Usage
value = fmt.Sprintf("%f", f.Value)
}
if f, ok := flag.(GenericFlag); ok {
description = f.Usage
if f.Value != nil {
value = f.Value.String()
}
}
if f, ok := flag.(Int64Flag); ok {
description = f.Usage
value = fmt.Sprintf("%d", f.Value)
}
if f, ok := flag.(IntFlag); ok {
description = f.Usage
value = fmt.Sprintf("%d", f.Value)
}
if f, ok := flag.(IntSliceFlag); ok {
description = f.Usage
if f.Value != nil {
value = f.Value.String()
}
}
if f, ok := flag.(Int64SliceFlag); ok {
description = f.Usage
if f.Value != nil {
value = f.Value.String()
}
}
if f, ok := flag.(StringFlag); ok {
description = f.Usage
value = f.Value
}
if f, ok := flag.(StringSliceFlag); ok {
description = f.Usage
if f.Value != nil {
value = f.Value.String()
}
}
if f, ok := flag.(Uint64Flag); ok {
description = f.Usage
value = fmt.Sprintf("%d", f.Value)
}
if f, ok := flag.(UintFlag); ok {
description = f.Usage
value = fmt.Sprintf("%d", f.Value)
}
if description == "" {
description = noDescription
}
if value != "" {
description += " (default: " + value + ")"
}
return ": " + description
}
82 changes: 82 additions & 0 deletions docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package cli

import (
"testing"
)

func testApp() *App {
app := NewApp()
app.Name = "greet"
app.Flags = []Flag{
StringFlag{
Name: "socket, s",
Usage: "some usage text",
Value: "value",
},
StringFlag{Name: "flag, fl, f"},
BoolFlag{
Name: "another-flag, b",
Usage: "another usage text",
},
}
app.Commands = []Command{{
Aliases: []string{"c"},
Flags: []Flag{
StringFlag{Name: "flag, fl, f"},
BoolFlag{
Name: "another-flag, b",
Usage: "another usage text",
},
},
Name: "config",
Usage: "another usage test",
Subcommands: []Command{{
Aliases: []string{"s", "ss"},
Flags: []Flag{
StringFlag{Name: "sub-flag, sub-fl, s"},
BoolFlag{
Name: "sub-command-flag, s",
Usage: "some usage text",
},
},
Name: "sub-config",
Usage: "another usage test",
}},
}, {
Aliases: []string{"i", "in"},
Name: "info",
Usage: "retrieve generic information",
}, {
Name: "some-command",
}}
app.UsageText = "app [first_arg] [second_arg]"
app.Usage = "Some app"
app.Author = "Harrison"
app.Email = "harrison@lolwut.com"
app.Authors = []Author{{Name: "Oliver Allen", Email: "oliver@toyshop.com"}}
return app
}

func TestToMarkdown(t *testing.T) {
// Given
app := testApp()

// When
_, err := app.ToMarkdown()

// Then
// TODO: extend test case
expect(t, err, nil)
}

func TestToMan(t *testing.T) {
// Given
app := testApp()

// When
_, err := app.ToMan()

// Then
// TODO: extend test case
expect(t, err, nil)
}

0 comments on commit 641aae9

Please sign in to comment.