Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions lib/docgen/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package docgen

import (
"bytes"

"github.com/urfave/cli/v3"
)

// Default renders help text for the app using urfave/cli's default help format.
func Default(cmd *cli.Command) (string, error) {
tpl := cmd.CustomRootCommandHelpTemplate
if tpl == "" {
tpl = cli.RootCommandHelpTemplate
}

var w bytes.Buffer
cli.HelpPrinterCustom(&w, tpl, cmd, nil)
return w.String(), nil
}
193 changes: 193 additions & 0 deletions lib/docgen/markdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package docgen

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

"github.com/urfave/cli/v3"
)

// Markdown renders a Markdown reference for the app.
//
// It is adapted from https://sourcegraph.com/github.com/urfave/cli-docs/-/blob/docs.go?L16
func Markdown(root *cli.Command) (string, error) {
var w bytes.Buffer
if err := writeDocTemplate(root, &w); err != nil {
return "", err
}
return w.String(), nil
}

type cliTemplate struct {
App *cli.Command
Commands []string
GlobalArgs []string
}

func writeDocTemplate(root *cli.Command, 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: root,
Commands: prepareCommands(root.Name, root.Commands, 0),
GlobalArgs: prepareArgsWithValues(root.VisibleFlags()),
})
}

func prepareCommands(lineage string, commands []*cli.Command, level int) []string {
var coms []string
for _, command := range commands {
if command.Hidden {
continue
}

var commandDoc strings.Builder
commandDoc.WriteString(strings.Repeat("#", level+2))
commandDoc.WriteString(" ")
commandDoc.WriteString(fmt.Sprintf("%s %s", lineage, command.Name))
commandDoc.WriteString("\n\n")
commandDoc.WriteString(prepareUsage(command))
commandDoc.WriteString("\n\n")

if len(command.Description) > 0 {
commandDoc.WriteString(fmt.Sprintf("%s\n\n", command.Description))
}

commandDoc.WriteString(prepareUsageText(command))

flags := prepareArgsWithValues(command.Flags)
if len(flags) > 0 {
commandDoc.WriteString("\nFlags:\n\n")
for _, f := range flags {
commandDoc.WriteString("* " + f)
}
}

coms = append(coms, commandDoc.String())

// recursevly iterate subcommands
if len(command.Commands) > 0 {
coms = append(
coms,
prepareCommands(lineage+" "+command.Name, command.Commands, level+1)...,
)
}
}

return coms
}

func prepareArgsWithValues(flags []cli.Flag) []string {
return prepareFlags(flags, ", ", "`", "`", `"<value>"`, true)
}

func prepareFlags(
flags []cli.Flag,
sep, opener, closer, value string,
addDetails bool,
) []string {
args := []string{}
for _, f := range flags {
flag, ok := f.(cli.DocGenerationFlag)
if !ok {
continue
}
modifiedArg := opener

for _, s := range f.Names() {
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)
}
}

if flag.TakesValue() {
modifiedArg += fmt.Sprintf("=%s", value)
}

modifiedArg += closer

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 cli.DocGenerationFlag) string {
description := flag.GetUsage()
value := flag.GetValue()
if value != "" {
description += " (default: " + value + ")"
}
return ": " + description
}

func prepareUsageText(command *cli.Command) string {
if command.UsageText == "" {
if strings.TrimSpace(command.ArgsUsage) != "" {
return fmt.Sprintf("Arguments: `%s`\n", command.ArgsUsage)
}
return ""
}

// Write all usage examples as a big shell code block
var usageText strings.Builder
usageText.WriteString("```sh")
for line := range strings.SplitSeq(strings.TrimSpace(command.UsageText), "\n") {
usageText.WriteByte('\n')

line = strings.TrimSpace(line)
if strings.HasPrefix(line, "# ") {
usageText.WriteString(line)
} else if len(line) > 0 {
usageText.WriteString(fmt.Sprintf("$ %s", line))
}
}
usageText.WriteString("\n```\n")

return usageText.String()
}

func prepareUsage(command *cli.Command) string {
if command.Usage == "" {
return ""
}

return command.Usage + "."
}

var markdownDocTemplate = `# {{ .App.Name }} reference

{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }}
{{ if .App.Description }}
{{ .App.Description }}
{{ end }}
` + "```sh" + `{{ if .App.UsageText }}
{{ .App.UsageText }}
{{ else }}
{{ .App.Name }} [GLOBAL FLAGS] command [COMMAND FLAGS] [ARGUMENTS...]
{{ end }}` + "```" + `
{{ if .GlobalArgs }}
Global flags:

{{ range $v := .GlobalArgs }}* {{ $v }}{{ end }}{{ end }}{{ if .Commands }}
{{ range $v := .Commands }}
{{ $v }}{{ end }}{{ end }}`
4 changes: 4 additions & 0 deletions lib/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/sourcegraph/conc v0.3.0
github.com/sourcegraph/go-diff v0.7.0
github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b
github.com/urfave/cli/v3 v3.8.0
github.com/xeipuuv/gojsonschema v1.2.0
github.com/xlab/treeprint v1.2.0
go.opentelemetry.io/otel v1.38.0
Expand Down Expand Up @@ -78,3 +79,6 @@ require (
golang.org/x/tools v0.37.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

// See: https://github.com/ghodss/yaml/pull/65
replace github.com/ghodss/yaml => github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152
7 changes: 5 additions & 2 deletions lib/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
Expand Down Expand Up @@ -141,11 +139,15 @@ github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCp
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b h1:2FQ72y5zECMu9e5z5jMnllb5n1jVK7qsvgjkVtdFV+g=
github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b/go.mod h1:IDp09QkoqS8Z3CyN2RW6vXjgABkNpDbyjLIHNQwQ8P8=
github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 h1:z/MpntplPaW6QW95pzcAR/72Z5TWDyDnSo0EOcyij9o=
github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
Expand Down Expand Up @@ -221,6 +223,7 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
Loading