-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add markdown and man page docs generation methods
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
1 parent
1169906
commit 93231c0
Showing
2 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |