diff --git a/docs.go b/docs.go index 45a0bc314d..af15575c97 100644 --- a/docs.go +++ b/docs.go @@ -7,13 +7,94 @@ import ( "bytes" "fmt" "io" + "os" + "regexp" "sort" + "strconv" "strings" "text/template" + "unicode/utf8" "github.com/cpuguy83/go-md2man/v2/md2man" ) +// ToTabularMarkdown creates a tabular markdown documentation for the `*App`. +// The function errors if either parsing or writing of the string fails. +func (a *App) ToTabularMarkdown(appPath string) (string, error) { + if appPath == "" { + appPath = "app" + } + + const name = "cli" + + t, err := template.New(name).Funcs(template.FuncMap{ + "join": strings.Join, + }).Parse(MarkdownTabularDocTemplate) + if err != nil { + return "", err + } + + var ( + w bytes.Buffer + tt tabularTemplate + ) + + if err = t.ExecuteTemplate(&w, name, cliTabularAppTemplate{ + AppPath: appPath, + Name: a.Name, + Description: tt.PrepareMultilineString(a.Description), + Usage: tt.PrepareMultilineString(a.Usage), + UsageText: strings.FieldsFunc(a.UsageText, func(r rune) bool { return r == '\n' }), + ArgsUsage: tt.PrepareMultilineString(a.ArgsUsage), + GlobalFlags: tt.PrepareFlags(a.VisibleFlags()), + Commands: tt.PrepareCommands(a.VisibleCommands(), appPath, "", 0), + }); err != nil { + return "", err + } + + return tt.Prettify(w.String()), nil +} + +// ToTabularToFileBetweenTags creates a tabular markdown documentation for the `*App` and updates the file between +// the tags in the file. The function errors if either parsing or writing of the string fails. +func (a *App) ToTabularToFileBetweenTags(appPath, filePath string, startEndTags ...string) error { + var start, end = "", "" // default tags + + if len(startEndTags) == 2 { + start, end = startEndTags[0], startEndTags[1] + } + + // read original file content + content, err := os.ReadFile(filePath) + if err != nil { + return err + } + + // generate markdown + md, err := a.ToTabularMarkdown(appPath) + if err != nil { + return err + } + + // prepare regexp to replace content between start and end tags + re, err := regexp.Compile("(?s)" + regexp.QuoteMeta(start) + "(.*?)" + regexp.QuoteMeta(end)) + if err != nil { + return err + } + + const comment = "" + + // replace content between start and end tags + updated := re.ReplaceAll(content, []byte(strings.Join([]string{start, comment, md, end}, "\n"))) + + // write updated content to file + if err = os.WriteFile(filePath, updated, 0664); err != nil { + return err + } + + return nil +} + // 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) { @@ -196,3 +277,237 @@ func prepareUsage(command *Command, usageText string) string { return usage } + +type ( + cliTabularAppTemplate struct { + AppPath string + Name string + Usage string + ArgsUsage string + UsageText []string + Description string + GlobalFlags []cliTabularFlagTemplate + Commands []cliTabularCommandTemplate + } + + cliTabularCommandTemplate struct { + AppPath string + Name string + Aliases []string + Usage string + ArgsUsage string + UsageText []string + Description string + Category string + Flags []cliTabularFlagTemplate + SubCommands []cliTabularCommandTemplate + Level uint + } + + cliTabularFlagTemplate struct { + Name string + Aliases []string + Usage string + TakesValue bool + Default string + EnvVars []string + } +) + +// tabularTemplate is a struct for the tabular template preparation. +type tabularTemplate struct{} + +// PrepareCommands converts CLI commands into a structs for the rendering. +func (tt tabularTemplate) PrepareCommands(commands []*Command, appPath, parentCommandName string, level uint) []cliTabularCommandTemplate { + var result = make([]cliTabularCommandTemplate, 0, len(commands)) + + for _, cmd := range commands { + var command = cliTabularCommandTemplate{ + AppPath: appPath, + Name: strings.TrimSpace(strings.Join([]string{parentCommandName, cmd.Name}, " ")), + Aliases: cmd.Aliases, + Usage: tt.PrepareMultilineString(cmd.Usage), + UsageText: strings.FieldsFunc(cmd.UsageText, func(r rune) bool { return r == '\n' }), + ArgsUsage: tt.PrepareMultilineString(cmd.ArgsUsage), + Description: tt.PrepareMultilineString(cmd.Description), + Category: cmd.Category, + Flags: tt.PrepareFlags(cmd.VisibleFlags()), + SubCommands: tt.PrepareCommands( // note: recursive call + cmd.Commands, + appPath, + strings.Join([]string{parentCommandName, cmd.Name}, " "), + level+1, + ), + Level: level, + } + + result = append(result, command) + } + + return result +} + +// PrepareFlags converts CLI flags into a structs for the rendering. +func (tt tabularTemplate) PrepareFlags(flags []Flag) []cliTabularFlagTemplate { + var result = make([]cliTabularFlagTemplate, 0, len(flags)) + + for _, appFlag := range flags { + flag, ok := appFlag.(DocGenerationFlag) + if !ok { + continue + } + + var f = cliTabularFlagTemplate{ + Usage: tt.PrepareMultilineString(flag.GetUsage()), + EnvVars: flag.GetEnvVars(), + TakesValue: flag.TakesValue(), + Default: flag.GetValue(), + } + + if boolFlag, isBool := appFlag.(*BoolFlag); isBool { + f.Default = strconv.FormatBool(boolFlag.Value) + } + + for i, name := range flag.Names() { + name = strings.TrimSpace(name) + + if i == 0 { + f.Name = "--" + name + + continue + } + + if len(name) > 1 { + name = "--" + name + } else { + name = "-" + name + } + + f.Aliases = append(f.Aliases, name) + } + + result = append(result, f) + } + + return result +} + +// PrepareMultilineString prepares a string (removes line breaks). +func (tabularTemplate) PrepareMultilineString(s string) string { + return strings.TrimRight( + strings.TrimSpace( + strings.ReplaceAll(s, "\n", " "), + ), + ".\r\n\t", + ) +} + +func (tabularTemplate) Prettify(s string) string { + var max = func(x, y int) int { + if x > y { + return x + } + return y + } + + var b strings.Builder + + // search for tables + for _, rawTable := range regexp.MustCompile(`(?m)^(\|[^\n]+\|\r?\n)((?:\|:?-+:?)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$`).FindAllString(s, -1) { + var lines = strings.FieldsFunc(rawTable, func(r rune) bool { return r == '\n' }) + + if len(lines) < 3 { // header, separator, body + continue + } + + // parse table into the matrix + var matrix = make([][]string, 0, len(lines)) + for _, line := range lines { + items := strings.FieldsFunc(strings.Trim(line, "| "), func(r rune) bool { return r == '|' }) + + for i := range items { + items[i] = strings.TrimSpace(items[i]) // trim spaces in cells + } + + matrix = append(matrix, items) + } + + // determine centered columns + var centered = make([]bool, 0, len(matrix[1])) + for _, cell := range matrix[1] { + centered = append(centered, strings.HasPrefix(cell, ":") && strings.HasSuffix(cell, ":")) + } + + // calculate max lengths + var lengths = make([]int, len(matrix[0])) + for n, row := range matrix { + for i, cell := range row { + if n == 1 { + continue // skip separator + } + + if l := utf8.RuneCountInString(cell); l > lengths[i] { + lengths[i] = l + } + } + } + + // format cells + for i, row := range matrix { + for j, cell := range row { + if i == 1 { // is separator + if centered[j] { + b.Reset() + b.WriteRune(':') + b.WriteString(strings.Repeat("-", max(0, lengths[j]))) + b.WriteRune(':') + + row[j] = b.String() + } else { + row[j] = strings.Repeat("-", max(0, lengths[j]+2)) + } + + continue + } + + var ( + cellWidth = utf8.RuneCountInString(cell) + padLeft, padRight = 1, max(1, lengths[j]-cellWidth+1) // align to the left + ) + + if centered[j] { // is centered + padLeft = max(1, (lengths[j]-cellWidth)/2) + padRight = max(1, lengths[j]-cellWidth-(padLeft-1)) + } + + b.Reset() + b.WriteString(strings.Repeat(" ", padLeft)) + + if padLeft+cellWidth+padRight <= lengths[j]+1 { + b.WriteRune(' ') // add an extra space if the cell is not full + } + + b.WriteString(cell) + b.WriteString(strings.Repeat(" ", padRight)) + + row[j] = b.String() + } + } + + b.Reset() + + for _, row := range matrix { // build new table + b.WriteRune('|') + b.WriteString(strings.Join(row, "|")) + b.WriteRune('|') + b.WriteRune('\n') + } + + s = strings.Replace(s, rawTable, b.String(), 1) + } + + s = regexp.MustCompile(`\n{2,}`).ReplaceAllString(s, "\n\n") // normalize newlines + s = strings.Trim(s, " \n") // trim spaces and newlines + + return s + "\n" // add an extra newline +} diff --git a/docs_test.go b/docs_test.go index e4ae2fe7b4..f6ac4bead3 100644 --- a/docs_test.go +++ b/docs_test.go @@ -4,7 +4,10 @@ package cli import ( + "bytes" "errors" + "io/fs" + "os" "testing" ) @@ -20,6 +23,157 @@ func TestToMarkdownFull(t *testing.T) { expectFileContent(t, "testdata/expected-doc-full.md", res) } +func TestToTabularMarkdown(t *testing.T) { + app := testApp() + + t.Run("full", func(t *testing.T) { + // When + res, err := app.ToTabularMarkdown("app") + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-tabular-markdown-full.md", res) + }) + + t.Run("with empty path", func(t *testing.T) { + // When + res, err := app.ToTabularMarkdown("") + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-tabular-markdown-full.md", res) + }) + + t.Run("with custom app path", func(t *testing.T) { + // When + res, err := app.ToTabularMarkdown("/usr/local/bin") + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-tabular-markdown-custom-app-path.md", res) + }) +} + +func TestToTabularMarkdownFailed(t *testing.T) { + tpl := MarkdownTabularDocTemplate + defer func() { MarkdownTabularDocTemplate = tpl }() // restore + + MarkdownTabularDocTemplate = "{{ .Foo }}" // invalid template + + // Given + app := testApp() + + // When + res, err := app.ToTabularMarkdown("") + + // Then + if err == nil { + t.Fatal("Expected error but got nil") + } + expect(t, res, "") +} + +func TestToTabularToFileBetweenTags(t *testing.T) { + expectedDocs, fErr := os.ReadFile("testdata/expected-tabular-markdown-full.md") + expect(t, fErr, nil) // read without error + + // normalizes \r\n (windows) and \r (mac) into \n (unix) (required for tests to pass on windows) + var normalizeNewlines = func(d []byte) []byte { + d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1) // replace CR LF \r\n (windows) with LF \n (unix) + d = bytes.Replace(d, []byte{13}, []byte{10}, -1) // replace CF \r (mac) with LF \n (unix) + + return d + } + + t.Run("default tags", func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "") + expect(t, err, nil) // created without error + + defer func() { expect(t, os.Remove(tmpFile.Name()), nil) }() // cleanup + + _, err = tmpFile.WriteString(`# App readme file + +Some description + + + + +Some other text`) + expect(t, err, nil) // wrote without error + _ = tmpFile.Close() + + expect(t, testApp().ToTabularToFileBetweenTags("app", tmpFile.Name()), nil) // replaced without error + + content, err := os.ReadFile(tmpFile.Name()) // read the file content + expect(t, err, nil) + + content = normalizeNewlines(content) + + expected := normalizeNewlines([]byte(`# App readme file + +Some description + + + +` + string(expectedDocs) + ` + + +Some other text`)) + + expect(t, string(content), string(expected)) // content matches + }) + + t.Run("custom tags", func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "") + expect(t, err, nil) // created without error + + defer func() { expect(t, os.Remove(tmpFile.Name()), nil) }() // cleanup + + _, err = tmpFile.WriteString(`# App readme file + +Some description + +foo_BAR|baz +lorem+ipsum + +Some other text`) + expect(t, err, nil) // wrote without error + _ = tmpFile.Close() + + expect(t, testApp().ToTabularToFileBetweenTags("app", tmpFile.Name(), "foo_BAR|baz", "lorem+ipsum"), nil) + + content, err := os.ReadFile(tmpFile.Name()) // read the file content + expect(t, err, nil) + + content = normalizeNewlines(content) + + expected := normalizeNewlines([]byte(`# App readme file + +Some description + +foo_BAR|baz + +` + string(expectedDocs) + ` +lorem+ipsum + +Some other text`)) + + expect(t, string(content), string(expected)) // content matches + }) + + t.Run("missing file", func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "") + expect(t, err, nil) // created without error + _ = tmpFile.Close() + + expect(t, os.Remove(tmpFile.Name()), nil) // and remove immediately + + err = testApp().ToTabularToFileBetweenTags("app", tmpFile.Name()) + + expect(t, errors.Is(err, fs.ErrNotExist), true) + }) +} + func TestToMarkdownNoFlags(t *testing.T) { // Given app := testApp() diff --git a/fish_test.go b/fish_test.go index de75939614..2826ba8d3e 100644 --- a/fish_test.go +++ b/fish_test.go @@ -39,6 +39,7 @@ func testApp() *App { Name: "another-flag", Aliases: []string{"b"}, Usage: "another usage text", + EnvVars: []string{"EXAMPLE_VARIABLE_NAME"}, }, &BoolFlag{ Name: "hidden-flag", diff --git a/godoc-current.txt b/godoc-current.txt index 8a8158c487..6b7a4ee1a3 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -131,6 +131,77 @@ var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionN # COMMANDS {{ range $v := .Commands }} {{ $v }}{{ end }}{{ end }}` +var MarkdownTabularDocTemplate = `{{ define "flags" }} +| Name | Description | Default value | Environment variables | +|------|-------------|:-------------:|:---------------------:| +{{ range $flag := . -}} +{{- /**/ -}} | ` + "`" + `{{ $flag.Name }}{{ if $flag.TakesValue }}="…"{{ end }}` + "`" + ` {{ if $flag.Aliases }}(` + "`" + `{{ join $flag.Aliases "` + "`, `" + `" }}` + "`" + `) {{ end }} +{{- /**/ -}} | {{ $flag.Usage }} +{{- /**/ -}} | {{ if $flag.Default }}` + "`" + `{{ $flag.Default }}` + "`" + `{{ end }} +{{- /**/ -}} | {{ if $flag.EnvVars }}` + "`" + `{{ join $flag.EnvVars "` + "`, `" + `" }}` + "`" + `{{ else }}*none*{{ end }} +{{- /**/ -}} | +{{ end }} +{{ end }} + +{{ define "command" }} +### ` + "`" + `{{ .Name }}` + "`" + ` {{ if gt .Level 0 }}sub{{ end }}command{{ if .Aliases }} (aliases: ` + "`" + `{{ join .Aliases "` + "`, `" + `" }}` + "`" + `){{ end }} +{{ if .Usage }} +{{ .Usage }}. +{{ end }} +{{ if .UsageText }} +{{ range $line := .UsageText -}} +> {{ $line }} +{{ end -}} +{{ end }} +{{ if .Description }} +{{ .Description }}. +{{ end }} +Usage: + +` + "```" + `bash +$ {{ .AppPath }} [GLOBAL FLAGS] {{ .Name }}{{ if .Flags }} [COMMAND FLAGS]{{ end }} {{ if .ArgsUsage }}{{ .ArgsUsage }}{{ else }}[ARGUMENTS...]{{ end }} +` + "```" + ` + +{{ if .Flags -}} +The following flags are supported: +{{ template "flags" .Flags }} +{{ end -}} + +{{ if .SubCommands -}} +{{ range $subCmd := .SubCommands -}} +{{ template "command" $subCmd }} +{{ end -}} +{{ end -}} +{{ end }} + +## CLI interface{{ if .Name }} - {{ .Name }}{{ end }} + +{{ if .Description }}{{ .Description }}. +{{ end }} +{{ if .Usage }}{{ .Usage }}. +{{ end }} +{{ if .UsageText }} +{{ range $line := .UsageText -}} +> {{ $line }} +{{ end -}} +{{ end }} +Usage: + +` + "```" + `bash +$ {{ .AppPath }}{{ if .GlobalFlags }} [GLOBAL FLAGS]{{ end }} [COMMAND] [COMMAND FLAGS] {{ if .ArgsUsage }}{{ .ArgsUsage }}{{ else }}[ARGUMENTS...]{{ end }} +` + "```" + ` + +{{ if .GlobalFlags }} +Global flags: + +{{ template "flags" .GlobalFlags }} + +{{ end -}} +{{ if .Commands -}} +{{ range $cmd := .Commands -}} +{{ template "command" $cmd }} +{{ end }} +{{- end }}` var NewFloat64Slice = NewSliceBase[float64, NoConfig, float64Value] var NewInt64Slice = NewSliceBase[int64, IntegerConfig, int64Value] var NewIntSlice = NewSliceBase[int, IntegerConfig, intValue] @@ -378,6 +449,15 @@ func (a *App) ToMarkdown() (string, error) ToMarkdown creates a markdown string for the `*App` The function errors if either parsing or writing of the string fails. +func (a *App) ToTabularMarkdown(appPath string) (string, error) + ToTabularMarkdown creates a tabular markdown documentation for the `*App`. + The function errors if either parsing or writing of the string fails. + +func (a *App) ToTabularToFileBetweenTags(appPath, filePath string, startEndTags ...string) error + ToTabularToFileBetweenTags creates a tabular markdown documentation for + the `*App` and updates the file between the tags in the file. The function + errors if either parsing or writing of the string fails. + func (a *App) VisibleCategories() []CommandCategory VisibleCategories returns a slice of categories and commands that are Hidden=false diff --git a/template.go b/template.go index 85e066c833..87695aab52 100644 --- a/template.go +++ b/template.go @@ -128,6 +128,78 @@ var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionN {{ range $v := .Commands }} {{ $v }}{{ end }}{{ end }}` +var MarkdownTabularDocTemplate = `{{ define "flags" }} +| Name | Description | Default value | Environment variables | +|------|-------------|:-------------:|:---------------------:| +{{ range $flag := . -}} +{{- /**/ -}} | ` + "`" + `{{ $flag.Name }}{{ if $flag.TakesValue }}="…"{{ end }}` + "`" + ` {{ if $flag.Aliases }}(` + "`" + `{{ join $flag.Aliases "` + "`, `" + `" }}` + "`" + `) {{ end }} +{{- /**/ -}} | {{ $flag.Usage }} +{{- /**/ -}} | {{ if $flag.Default }}` + "`" + `{{ $flag.Default }}` + "`" + `{{ end }} +{{- /**/ -}} | {{ if $flag.EnvVars }}` + "`" + `{{ join $flag.EnvVars "` + "`, `" + `" }}` + "`" + `{{ else }}*none*{{ end }} +{{- /**/ -}} | +{{ end }} +{{ end }} + +{{ define "command" }} +### ` + "`" + `{{ .Name }}` + "`" + ` {{ if gt .Level 0 }}sub{{ end }}command{{ if .Aliases }} (aliases: ` + "`" + `{{ join .Aliases "` + "`, `" + `" }}` + "`" + `){{ end }} +{{ if .Usage }} +{{ .Usage }}. +{{ end }} +{{ if .UsageText }} +{{ range $line := .UsageText -}} +> {{ $line }} +{{ end -}} +{{ end }} +{{ if .Description }} +{{ .Description }}. +{{ end }} +Usage: + +` + "```" + `bash +$ {{ .AppPath }} [GLOBAL FLAGS] {{ .Name }}{{ if .Flags }} [COMMAND FLAGS]{{ end }} {{ if .ArgsUsage }}{{ .ArgsUsage }}{{ else }}[ARGUMENTS...]{{ end }} +` + "```" + ` + +{{ if .Flags -}} +The following flags are supported: +{{ template "flags" .Flags }} +{{ end -}} + +{{ if .SubCommands -}} +{{ range $subCmd := .SubCommands -}} +{{ template "command" $subCmd }} +{{ end -}} +{{ end -}} +{{ end }} + +## CLI interface{{ if .Name }} - {{ .Name }}{{ end }} + +{{ if .Description }}{{ .Description }}. +{{ end }} +{{ if .Usage }}{{ .Usage }}. +{{ end }} +{{ if .UsageText }} +{{ range $line := .UsageText -}} +> {{ $line }} +{{ end -}} +{{ end }} +Usage: + +` + "```" + `bash +$ {{ .AppPath }}{{ if .GlobalFlags }} [GLOBAL FLAGS]{{ end }} [COMMAND] [COMMAND FLAGS] {{ if .ArgsUsage }}{{ .ArgsUsage }}{{ else }}[ARGUMENTS...]{{ end }} +` + "```" + ` + +{{ if .GlobalFlags }} +Global flags: + +{{ template "flags" .GlobalFlags }} + +{{ end -}} +{{ if .Commands -}} +{{ range $cmd := .Commands -}} +{{ template "command" $cmd }} +{{ end }} +{{- end }}` + var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' diff --git a/testdata/expected-tabular-markdown-custom-app-path.md b/testdata/expected-tabular-markdown-custom-app-path.md new file mode 100644 index 0000000000..5aad2dfb98 --- /dev/null +++ b/testdata/expected-tabular-markdown-custom-app-path.md @@ -0,0 +1,117 @@ +## CLI interface - greet + +Description of the application. + +Some app. + +> app [first_arg] [second_arg] + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...] +``` + +Global flags: + +| Name | Description | Default value | Environment variables | +|-----------------------------|--------------------|:-------------:|:-----------------------:| +| `--socket="…"` (`-s`) | some 'usage' text | `value` | *none* | +| `--flag="…"` (`--fl`, `-f`) | | | *none* | +| `--another-flag` (`-b`) | another usage text | `false` | `EXAMPLE_VARIABLE_NAME` | + +### `config` command (aliases: `c`) + +another usage test. + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] config [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-----------------------------|--------------------|:-------------:|:---------------------:| +| `--flag="…"` (`--fl`, `-f`) | | | *none* | +| `--another-flag` (`-b`) | another usage text | `false` | *none* | + +### `config sub-config` subcommand (aliases: `s`, `ss`) + +another usage test. + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] config sub-config [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-------------------------------------|-----------------|:-------------:|:---------------------:| +| `--sub-flag="…"` (`--sub-fl`, `-s`) | | | *none* | +| `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | + +### `info` command (aliases: `i`, `in`) + +retrieve generic information. + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] info [ARGUMENTS...] +``` + +### `some-command` command + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] some-command [ARGUMENTS...] +``` + +### `usage` command (aliases: `u`) + +standard usage text. + +> Usage for the usage text +> - formatted: Based on the specified ConfigMap and summon secrets.yml +> - list: Inspect the environment for a specific process running on a Pod +> - for_effect: Compare 'namespace' environment with 'local' +> ``` +> func() { ... } +> ``` +> Should be a part of the same code block + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] usage [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-----------------------------|--------------------|:-------------:|:---------------------:| +| `--flag="…"` (`--fl`, `-f`) | | | *none* | +| `--another-flag` (`-b`) | another usage text | `false` | *none* | + +### `usage sub-usage` subcommand (aliases: `su`) + +standard usage text. + +> Single line of UsageText + +Usage: + +```bash +$ /usr/local/bin [GLOBAL FLAGS] usage sub-usage [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-----------------------------|-----------------|:-------------:|:---------------------:| +| `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | diff --git a/testdata/expected-tabular-markdown-full.md b/testdata/expected-tabular-markdown-full.md new file mode 100644 index 0000000000..2828e8462a --- /dev/null +++ b/testdata/expected-tabular-markdown-full.md @@ -0,0 +1,117 @@ +## CLI interface - greet + +Description of the application. + +Some app. + +> app [first_arg] [second_arg] + +Usage: + +```bash +$ app [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...] +``` + +Global flags: + +| Name | Description | Default value | Environment variables | +|-----------------------------|--------------------|:-------------:|:-----------------------:| +| `--socket="…"` (`-s`) | some 'usage' text | `value` | *none* | +| `--flag="…"` (`--fl`, `-f`) | | | *none* | +| `--another-flag` (`-b`) | another usage text | `false` | `EXAMPLE_VARIABLE_NAME` | + +### `config` command (aliases: `c`) + +another usage test. + +Usage: + +```bash +$ app [GLOBAL FLAGS] config [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-----------------------------|--------------------|:-------------:|:---------------------:| +| `--flag="…"` (`--fl`, `-f`) | | | *none* | +| `--another-flag` (`-b`) | another usage text | `false` | *none* | + +### `config sub-config` subcommand (aliases: `s`, `ss`) + +another usage test. + +Usage: + +```bash +$ app [GLOBAL FLAGS] config sub-config [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-------------------------------------|-----------------|:-------------:|:---------------------:| +| `--sub-flag="…"` (`--sub-fl`, `-s`) | | | *none* | +| `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | + +### `info` command (aliases: `i`, `in`) + +retrieve generic information. + +Usage: + +```bash +$ app [GLOBAL FLAGS] info [ARGUMENTS...] +``` + +### `some-command` command + +Usage: + +```bash +$ app [GLOBAL FLAGS] some-command [ARGUMENTS...] +``` + +### `usage` command (aliases: `u`) + +standard usage text. + +> Usage for the usage text +> - formatted: Based on the specified ConfigMap and summon secrets.yml +> - list: Inspect the environment for a specific process running on a Pod +> - for_effect: Compare 'namespace' environment with 'local' +> ``` +> func() { ... } +> ``` +> Should be a part of the same code block + +Usage: + +```bash +$ app [GLOBAL FLAGS] usage [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-----------------------------|--------------------|:-------------:|:---------------------:| +| `--flag="…"` (`--fl`, `-f`) | | | *none* | +| `--another-flag` (`-b`) | another usage text | `false` | *none* | + +### `usage sub-usage` subcommand (aliases: `su`) + +standard usage text. + +> Single line of UsageText + +Usage: + +```bash +$ app [GLOBAL FLAGS] usage sub-usage [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Default value | Environment variables | +|-----------------------------|-----------------|:-------------:|:---------------------:| +| `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 8a8158c487..6b7a4ee1a3 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -131,6 +131,77 @@ var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionN # COMMANDS {{ range $v := .Commands }} {{ $v }}{{ end }}{{ end }}` +var MarkdownTabularDocTemplate = `{{ define "flags" }} +| Name | Description | Default value | Environment variables | +|------|-------------|:-------------:|:---------------------:| +{{ range $flag := . -}} +{{- /**/ -}} | ` + "`" + `{{ $flag.Name }}{{ if $flag.TakesValue }}="…"{{ end }}` + "`" + ` {{ if $flag.Aliases }}(` + "`" + `{{ join $flag.Aliases "` + "`, `" + `" }}` + "`" + `) {{ end }} +{{- /**/ -}} | {{ $flag.Usage }} +{{- /**/ -}} | {{ if $flag.Default }}` + "`" + `{{ $flag.Default }}` + "`" + `{{ end }} +{{- /**/ -}} | {{ if $flag.EnvVars }}` + "`" + `{{ join $flag.EnvVars "` + "`, `" + `" }}` + "`" + `{{ else }}*none*{{ end }} +{{- /**/ -}} | +{{ end }} +{{ end }} + +{{ define "command" }} +### ` + "`" + `{{ .Name }}` + "`" + ` {{ if gt .Level 0 }}sub{{ end }}command{{ if .Aliases }} (aliases: ` + "`" + `{{ join .Aliases "` + "`, `" + `" }}` + "`" + `){{ end }} +{{ if .Usage }} +{{ .Usage }}. +{{ end }} +{{ if .UsageText }} +{{ range $line := .UsageText -}} +> {{ $line }} +{{ end -}} +{{ end }} +{{ if .Description }} +{{ .Description }}. +{{ end }} +Usage: + +` + "```" + `bash +$ {{ .AppPath }} [GLOBAL FLAGS] {{ .Name }}{{ if .Flags }} [COMMAND FLAGS]{{ end }} {{ if .ArgsUsage }}{{ .ArgsUsage }}{{ else }}[ARGUMENTS...]{{ end }} +` + "```" + ` + +{{ if .Flags -}} +The following flags are supported: +{{ template "flags" .Flags }} +{{ end -}} + +{{ if .SubCommands -}} +{{ range $subCmd := .SubCommands -}} +{{ template "command" $subCmd }} +{{ end -}} +{{ end -}} +{{ end }} + +## CLI interface{{ if .Name }} - {{ .Name }}{{ end }} + +{{ if .Description }}{{ .Description }}. +{{ end }} +{{ if .Usage }}{{ .Usage }}. +{{ end }} +{{ if .UsageText }} +{{ range $line := .UsageText -}} +> {{ $line }} +{{ end -}} +{{ end }} +Usage: + +` + "```" + `bash +$ {{ .AppPath }}{{ if .GlobalFlags }} [GLOBAL FLAGS]{{ end }} [COMMAND] [COMMAND FLAGS] {{ if .ArgsUsage }}{{ .ArgsUsage }}{{ else }}[ARGUMENTS...]{{ end }} +` + "```" + ` + +{{ if .GlobalFlags }} +Global flags: + +{{ template "flags" .GlobalFlags }} + +{{ end -}} +{{ if .Commands -}} +{{ range $cmd := .Commands -}} +{{ template "command" $cmd }} +{{ end }} +{{- end }}` var NewFloat64Slice = NewSliceBase[float64, NoConfig, float64Value] var NewInt64Slice = NewSliceBase[int64, IntegerConfig, int64Value] var NewIntSlice = NewSliceBase[int, IntegerConfig, intValue] @@ -378,6 +449,15 @@ func (a *App) ToMarkdown() (string, error) ToMarkdown creates a markdown string for the `*App` The function errors if either parsing or writing of the string fails. +func (a *App) ToTabularMarkdown(appPath string) (string, error) + ToTabularMarkdown creates a tabular markdown documentation for the `*App`. + The function errors if either parsing or writing of the string fails. + +func (a *App) ToTabularToFileBetweenTags(appPath, filePath string, startEndTags ...string) error + ToTabularToFileBetweenTags creates a tabular markdown documentation for + the `*App` and updates the file between the tags in the file. The function + errors if either parsing or writing of the string fails. + func (a *App) VisibleCategories() []CommandCategory VisibleCategories returns a slice of categories and commands that are Hidden=false