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 7, 2019
1 parent 97179ca commit 40d4a25
Show file tree
Hide file tree
Showing 17 changed files with 908 additions and 103 deletions.
36 changes: 24 additions & 12 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) {
{
testCase: "error_case_empty_input_with_required_flag_on_command",
appRunInput: []string{"myCLI", "myCommand"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
Expand All @@ -987,9 +987,9 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) {
{
testCase: "error_case_empty_input_with_required_flag_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Subcommands: []Command{Command{
Subcommands: []Command{{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
Expand All @@ -1005,17 +1005,17 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) {
{
testCase: "valid_case_help_input_with_required_flag_on_command",
appRunInput: []string{"myCLI", "myCommand", "--help"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
},
{
testCase: "valid_case_help_input_with_required_flag_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--help"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Subcommands: []Command{Command{
Subcommands: []Command{{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
Expand All @@ -1031,7 +1031,7 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) {
{
testCase: "error_case_optional_input_with_required_flag_on_command",
appRunInput: []string{"myCLI", "myCommand", "--optional", "cats"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}},
}},
Expand All @@ -1040,9 +1040,9 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) {
{
testCase: "error_case_optional_input_with_required_flag_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--optional", "cats"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Subcommands: []Command{Command{
Subcommands: []Command{{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}},
}},
Expand All @@ -1058,17 +1058,17 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) {
{
testCase: "valid_case_required_flag_input_on_command",
appRunInput: []string{"myCLI", "myCommand", "--requiredFlag", "cats"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
},
{
testCase: "valid_case_required_flag_input_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--requiredFlag", "cats"},
appCommands: []Command{Command{
appCommands: []Command{{
Name: "myCommand",
Subcommands: []Command{Command{
Subcommands: []Command{{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
Expand Down Expand Up @@ -1825,6 +1825,18 @@ func (c *customBoolFlag) GetName() string {
return c.Nombre
}

func (c *customBoolFlag) TakesValue() bool {
return false
}

func (c *customBoolFlag) GetValue() string {
return "value"
}

func (c *customBoolFlag) GetUsage() string {
return "usage"
}

func (c *customBoolFlag) Apply(set *flag.FlagSet) {
set.String(c.Nombre, c.Nombre, "")
}
Expand Down
146 changes: 146 additions & 0 deletions docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package cli

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

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

// 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.writeDocTemplate(&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.writeDocTemplate(&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) writeDocTemplate(w io.Writer) error {
const name = "cli"
t, err := template.New(name).Parse(MarkdownDocTemplate)
if err != nil {
return err
}
return t.ExecuteTemplate(w, name, &cliTemplate{
App: a,
Commands: prepareCommands(a.Commands, 0),
GlobalArgs: prepareArgsWithValues(a.Flags),
SynopsisArgs: prepareArgsSynopsis(a.Flags),
})
}

func prepareCommands(commands []Command, level int) []string {
coms := []string{}
for i := range commands {
command := &commands[i]
usage := ""
if command.Usage != "" {
usage = command.Usage
}

prepared := fmt.Sprintf("%s %s\n\n%s\n",
strings.Repeat("#", level+2),
strings.Join(command.Names(), ", "),
usage,
)

flags := prepareArgsWithValues(command.Flags)
if len(flags) > 0 {
prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
}

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 _, f := range flags {
flag, ok := f.(DocGenerationFlag)
if !ok {
continue
}
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 += fmt.Sprintf("--%s", trimmed)
} else {
modifiedArg += fmt.Sprintf("-%s", trimmed)
}
}
modifiedArg += closer
if flag.TakesValue() {
modifiedArg += fmt.Sprintf("=%s", value)
}

if addDetails {
modifiedArg += flagDetails(flag)
}

args = append(args, modifiedArg+"\n")

}
sort.Strings(args)
return args
}

// flagDetails returns a string containing the flags metadata
func flagDetails(flag DocGenerationFlag) string {
description := flag.GetUsage()
value := flag.GetValue()
if value != "" {
description += " (default: " + value + ")"
}
return ": " + description
}
115 changes: 115 additions & 0 deletions docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package cli

import (
"io/ioutil"
"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 expectFileContent(t *testing.T, file, expected string) {
data, err := ioutil.ReadFile(file)
expect(t, err, nil)
expect(t, string(data), expected)
}

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

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

// Then
expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-full.md", res)
}

func TestToMarkdownNoFlags(t *testing.T) {
// Given
app := testApp()
app.Flags = nil

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

// Then
expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-no-flags.md", res)
}

func TestToMarkdownNoCommands(t *testing.T) {
// Given
app := testApp()
app.Commands = nil

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

// Then
expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-no-commands.md", res)
}

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

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

// Then
expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-full.man", res)
}
8 changes: 4 additions & 4 deletions flag-gen/assets_vfsdata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions flag-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type FlagType struct {
ContextType string `json:"context_type"`
Parser string `json:"parser"`
ParserCast string `json:"parser_cast"`
ValueString string `json:"valueString"`
}

func main() {
Expand Down
Loading

0 comments on commit 40d4a25

Please sign in to comment.