Skip to content

Commit

Permalink
Merge pull request #830 from saschagrunert/docs-gen
Browse files Browse the repository at this point in the history
Add markdown and man page docs generation methods
  • Loading branch information
AudriusButkevicius authored Aug 8, 2019
2 parents e0057bb + 99fad61 commit 2c477e7
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 2c477e7

Please sign in to comment.