-
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.
Merge pull request #830 from saschagrunert/docs-gen
Add markdown and man page docs generation methods
- Loading branch information
Showing
17 changed files
with
908 additions
and
103 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
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,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 | ||
} |
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,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) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
Oops, something went wrong.