diff --git a/docs.go b/docs.go new file mode 100644 index 0000000000..21fde022d0 --- /dev/null +++ b/docs.go @@ -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 +} diff --git a/docs_test.go b/docs_test.go new file mode 100644 index 0000000000..173555ddaf --- /dev/null +++ b/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) +}