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 4, 2019
1 parent 7745000 commit 5ad2b5d
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 88 deletions.
36 changes: 24 additions & 12 deletions app_test.go
Expand Up @@ -895,7 +895,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 @@ -904,9 +904,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 @@ -922,17 +922,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 @@ -948,7 +948,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 @@ -957,9 +957,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 @@ -975,17 +975,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 @@ -1742,6 +1742,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
153 changes: 153 additions & 0 deletions docs.go
@@ -0,0 +1,153 @@
package cli

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

"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 {
now := time.Now()
const name = "cli"
t, err := template.New(name).Parse(markdownDocTemplate)
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 flag.TakesValue() {
modifiedArg += "=" + value
}

if addDetails {
modifiedArg += flagDetails(flag)
}

args = append(args, modifiedArg+nl)

}
sort.Strings(args)
return args
}

// flagDetails returns a string containing the flags metadata
func flagDetails(flag Flag) string {
description := flag.GetUsage()
if flag.GetUsage() == "" {
description = noDescription
}
value := flag.GetValue()
if value != "" {
description += " (default: " + value + ")"
}
return ": " + description
}
82 changes: 82 additions & 0 deletions docs_test.go
@@ -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)
}
10 changes: 10 additions & 0 deletions flag.go
Expand Up @@ -73,6 +73,16 @@ type Flag interface {
// Apply Flag settings to the given flag set
Apply(*flag.FlagSet)
GetName() string

// TakesValue returns true of the flag takes a value, otherwise false
TakesValue() bool

// GetUsage returns the usage string for the flag
GetUsage() string

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
GetValue() string
}

// RequiredFlag is an interface that allows us to mark flags as required
Expand Down

0 comments on commit 5ad2b5d

Please sign in to comment.