Skip to content

Commit ba64bab

Browse files
committed
add cliutil/docgen from sourcegraph and adapted for v3
1 parent 568aa6d commit ba64bab

4 files changed

Lines changed: 221 additions & 2 deletions

File tree

lib/docgen/default.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package docgen
2+
3+
import (
4+
"bytes"
5+
6+
"github.com/urfave/cli/v3"
7+
)
8+
9+
// Default renders help text for the app using urfave/cli's default help format.
10+
func Default(cmd *cli.Command) (string, error) {
11+
tpl := cmd.CustomRootCommandHelpTemplate
12+
if tpl == "" {
13+
tpl = cli.RootCommandHelpTemplate
14+
}
15+
16+
var w bytes.Buffer
17+
cli.HelpPrinterCustom(&w, tpl, cmd, nil)
18+
return w.String(), nil
19+
}

lib/docgen/markdown.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package docgen
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"sort"
8+
"strings"
9+
"text/template"
10+
11+
"github.com/urfave/cli/v3"
12+
)
13+
14+
// Markdown renders a Markdown reference for the app.
15+
//
16+
// It is adapted from https://sourcegraph.com/github.com/urfave/cli-docs/-/blob/docs.go?L16
17+
func Markdown(root *cli.Command) (string, error) {
18+
var w bytes.Buffer
19+
if err := writeDocTemplate(root, &w); err != nil {
20+
return "", err
21+
}
22+
return w.String(), nil
23+
}
24+
25+
type cliTemplate struct {
26+
App *cli.Command
27+
Commands []string
28+
GlobalArgs []string
29+
}
30+
31+
func writeDocTemplate(root *cli.Command, w io.Writer) error {
32+
const name = "cli"
33+
t, err := template.New(name).Parse(markdownDocTemplate)
34+
if err != nil {
35+
return err
36+
}
37+
return t.ExecuteTemplate(w, name, &cliTemplate{
38+
App: root,
39+
Commands: prepareCommands(root.Name, root.Commands, 0),
40+
GlobalArgs: prepareArgsWithValues(root.VisibleFlags()),
41+
})
42+
}
43+
44+
func prepareCommands(lineage string, commands []*cli.Command, level int) []string {
45+
var coms []string
46+
for _, command := range commands {
47+
if command.Hidden {
48+
continue
49+
}
50+
51+
var commandDoc strings.Builder
52+
commandDoc.WriteString(strings.Repeat("#", level+2))
53+
commandDoc.WriteString(" ")
54+
commandDoc.WriteString(fmt.Sprintf("%s %s", lineage, command.Name))
55+
commandDoc.WriteString("\n\n")
56+
commandDoc.WriteString(prepareUsage(command))
57+
commandDoc.WriteString("\n\n")
58+
59+
if len(command.Description) > 0 {
60+
commandDoc.WriteString(fmt.Sprintf("%s\n\n", command.Description))
61+
}
62+
63+
commandDoc.WriteString(prepareUsageText(command))
64+
65+
flags := prepareArgsWithValues(command.Flags)
66+
if len(flags) > 0 {
67+
commandDoc.WriteString("\nFlags:\n\n")
68+
for _, f := range flags {
69+
commandDoc.WriteString("* " + f)
70+
}
71+
}
72+
73+
coms = append(coms, commandDoc.String())
74+
75+
// recursevly iterate subcommands
76+
if len(command.Commands) > 0 {
77+
coms = append(
78+
coms,
79+
prepareCommands(lineage+" "+command.Name, command.Commands, level+1)...,
80+
)
81+
}
82+
}
83+
84+
return coms
85+
}
86+
87+
func prepareArgsWithValues(flags []cli.Flag) []string {
88+
return prepareFlags(flags, ", ", "`", "`", `"<value>"`, true)
89+
}
90+
91+
func prepareFlags(
92+
flags []cli.Flag,
93+
sep, opener, closer, value string,
94+
addDetails bool,
95+
) []string {
96+
args := []string{}
97+
for _, f := range flags {
98+
flag, ok := f.(cli.DocGenerationFlag)
99+
if !ok {
100+
continue
101+
}
102+
modifiedArg := opener
103+
104+
for _, s := range f.Names() {
105+
trimmed := strings.TrimSpace(s)
106+
if len(modifiedArg) > len(opener) {
107+
modifiedArg += sep
108+
}
109+
if len(trimmed) > 1 {
110+
modifiedArg += fmt.Sprintf("--%s", trimmed)
111+
} else {
112+
modifiedArg += fmt.Sprintf("-%s", trimmed)
113+
}
114+
}
115+
116+
if flag.TakesValue() {
117+
modifiedArg += fmt.Sprintf("=%s", value)
118+
}
119+
120+
modifiedArg += closer
121+
122+
if addDetails {
123+
modifiedArg += flagDetails(flag)
124+
}
125+
126+
args = append(args, modifiedArg+"\n")
127+
128+
}
129+
sort.Strings(args)
130+
return args
131+
}
132+
133+
// flagDetails returns a string containing the flags metadata
134+
func flagDetails(flag cli.DocGenerationFlag) string {
135+
description := flag.GetUsage()
136+
value := flag.GetValue()
137+
if value != "" {
138+
description += " (default: " + value + ")"
139+
}
140+
return ": " + description
141+
}
142+
143+
func prepareUsageText(command *cli.Command) string {
144+
if command.UsageText == "" {
145+
if strings.TrimSpace(command.ArgsUsage) != "" {
146+
return fmt.Sprintf("Arguments: `%s`\n", command.ArgsUsage)
147+
}
148+
return ""
149+
}
150+
151+
// Write all usage examples as a big shell code block
152+
var usageText strings.Builder
153+
usageText.WriteString("```sh")
154+
for line := range strings.SplitSeq(strings.TrimSpace(command.UsageText), "\n") {
155+
usageText.WriteByte('\n')
156+
157+
line = strings.TrimSpace(line)
158+
if strings.HasPrefix(line, "# ") {
159+
usageText.WriteString(line)
160+
} else if len(line) > 0 {
161+
usageText.WriteString(fmt.Sprintf("$ %s", line))
162+
}
163+
}
164+
usageText.WriteString("\n```\n")
165+
166+
return usageText.String()
167+
}
168+
169+
func prepareUsage(command *cli.Command) string {
170+
if command.Usage == "" {
171+
return ""
172+
}
173+
174+
return command.Usage + "."
175+
}
176+
177+
var markdownDocTemplate = `# {{ .App.Name }} reference
178+
179+
{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }}
180+
{{ if .App.Description }}
181+
{{ .App.Description }}
182+
{{ end }}
183+
` + "```sh" + `{{ if .App.UsageText }}
184+
{{ .App.UsageText }}
185+
{{ else }}
186+
{{ .App.Name }} [GLOBAL FLAGS] command [COMMAND FLAGS] [ARGUMENTS...]
187+
{{ end }}` + "```" + `
188+
{{ if .GlobalArgs }}
189+
Global flags:
190+
191+
{{ range $v := .GlobalArgs }}* {{ $v }}{{ end }}{{ end }}{{ if .Commands }}
192+
{{ range $v := .Commands }}
193+
{{ $v }}{{ end }}{{ end }}`

lib/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/sourcegraph/conc v0.3.0
2323
github.com/sourcegraph/go-diff v0.7.0
2424
github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b
25+
github.com/urfave/cli/v3 v3.8.0
2526
github.com/xeipuuv/gojsonschema v1.2.0
2627
github.com/xlab/treeprint v1.2.0
2728
go.opentelemetry.io/otel v1.38.0
@@ -78,3 +79,6 @@ require (
7879
golang.org/x/tools v0.37.0 // indirect
7980
gopkg.in/yaml.v2 v2.4.0 // indirect
8081
)
82+
83+
// See: https://github.com/ghodss/yaml/pull/65
84+
replace github.com/ghodss/yaml => github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152

lib/go.sum

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
5252
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
5353
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
5454
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
55-
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
56-
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
5755
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
5856
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
5957
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -141,11 +139,15 @@ github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCp
141139
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
142140
github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b h1:2FQ72y5zECMu9e5z5jMnllb5n1jVK7qsvgjkVtdFV+g=
143141
github.com/sourcegraph/log v0.0.0-20250923023806-517b6960b55b/go.mod h1:IDp09QkoqS8Z3CyN2RW6vXjgABkNpDbyjLIHNQwQ8P8=
142+
github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 h1:z/MpntplPaW6QW95pzcAR/72Z5TWDyDnSo0EOcyij9o=
143+
github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
144144
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
145145
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
146146
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
147147
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
148148
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
149+
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
150+
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
149151
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
150152
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
151153
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -221,6 +223,7 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
221223
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
222224
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
223225
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
226+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
224227
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
225228
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
226229
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)