-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathnode_help.go
334 lines (323 loc) · 8.5 KB
/
node_help.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
package opts
import (
"bytes"
"fmt"
"log"
"os"
"regexp"
"sort"
"strings"
"text/template"
)
// data is only used for templating below
type data struct {
datum //data is also a datum
FlagGroups []*datumGroup
Args []*datum
Cmds []*datum
Order []string
Parents string
Version string
Summary string
Repo, Author string
ErrMsg string
}
type datum struct {
Name, Help, Pad string //Pad is Opt.padWidth many spaces
}
type datumGroup struct {
Name string
Flags []*datum
}
// DefaultOrder defines which templates get rendered in which order.
// This list is referenced in the "help" template below.
var DefaultOrder = []string{
"usage",
"summary",
"args",
"flaggroups",
"cmds",
"author",
"version",
"repo",
"errmsg",
}
func defaultOrder() []string {
order := make([]string, len(DefaultOrder))
copy(order, DefaultOrder)
return order
}
// DefaultTemplates define a set of individual templates
// that get rendered in DefaultOrder. You can replace templates or insert templates before or after existing
// templates using the DocSet, DocBefore and DocAfter methods. For example, you can insert a string after the
// usage text with:
//
// DocAfter("usage", "this is a string, and if it is very long, it will be wrapped")
//
// The entire help text is simply the "help" template listed below, which renders a set of these templates in
// the order defined above. All templates can be referenced using the keys in this map:
var DefaultTemplates = map[string]string{
"help": `{{ $root := . }}{{range $t := .Order}}{{ templ $t $root }}{{end}}`,
"usage": `Usage: {{.Name }} [options]{{template "usageargs" .}}{{template "usagecmd" .}}` + "\n",
"usageargs": `{{range .Args}} {{.Name}}{{end}}`,
"usagecmd": `{{if .Cmds}} <command>{{end}}`,
"extradefault": `{{if .}}default {{.}}{{end}}`,
"extraenv": `{{if .}}env {{.}}{{end}}`,
"extramultiple": `{{if .}}allows multiple{{end}}`,
"summary": "{{if .Summary}}\n{{ .Summary }}\n{{end}}",
"args": `{{range .Args}}{{template "arg" .}}{{end}}`,
"arg": "{{if .Help}}\n{{.Help}}\n{{end}}",
"flaggroups": `{{ range $g := .FlagGroups}}{{template "flaggroup" $g}}{{end}}`,
"flaggroup": "{{if .Flags}}\n{{if .Name}}{{.Name}} options{{else}}Options{{end}}:\n" +
`{{ range $f := .Flags}}{{template "flag" $f}}{{end}}{{end}}`,
"flag": `{{.Name}}{{if .Help}}{{.Pad}}{{.Help}}{{end}}` + "\n",
"cmds": "{{if .Cmds}}\nCommands:\n" + `{{ range $sub := .Cmds}}{{template "cmd" $sub}}{{end}}{{end}}`,
"cmd": "· {{ .Name }}{{if .Help}}{{.Pad}} {{ .Help }}{{end}}\n",
"version": "{{if .Version}}\nVersion:\n{{.Pad}}{{.Version}}\n{{end}}",
"repo": "{{if .Repo}}\nRead more:\n{{.Pad}}{{.Repo}}\n{{end}}",
"author": "{{if .Author}}\nAuthor:\n{{.Pad}}{{.Author}}\n{{end}}",
"errmsg": "{{if .ErrMsg}}\nError:\n{{.Pad}}{{.ErrMsg}}\n{{end}}",
}
var (
trailingSpaces = regexp.MustCompile(`(?m)\ +$`)
trailingBrackets = regexp.MustCompile(`^(.+)\(([^\)]+)\)$`)
)
// Help renders the help text as a string
func (o *node) Help() string {
h, err := renderHelp(o)
if err != nil {
log.Fatalf("render help failed: %s", err)
}
return h
}
func renderHelp(o *node) (string, error) {
var err error
//add default templates
for name, str := range DefaultTemplates {
if _, ok := o.templates[name]; !ok {
o.templates[name] = str
}
}
//prepare templates
t := template.New(o.name)
t = t.Funcs(map[string]interface{}{
//reimplementation of "template" except with dynamic name
"templ": func(name string, data interface{}) (string, error) {
b := &bytes.Buffer{}
err = t.ExecuteTemplate(b, name, data)
if err != nil {
return "", err
}
return b.String(), nil
},
})
//parse all templates and "define" themselves as nested templates
for name, str := range o.templates {
t, err = t.Parse(fmt.Sprintf(`{{define "%s"}}%s{{end}}`, name, str))
if err != nil {
return "", fmt.Errorf("template '%s': %s", name, err)
}
}
//convert node into template data
tf, err := convert(o)
if err != nil {
return "", fmt.Errorf("node convert: %s", err)
}
//execute all templates
b := &bytes.Buffer{}
err = t.ExecuteTemplate(b, "help", tf)
if err != nil {
return "", fmt.Errorf("template execute: %s", err)
}
out := b.String()
if o.padAll {
/*
"foo
bar"
becomes
"
foo
bar
"
*/
lines := strings.Split(out, "\n")
for i, l := range lines {
lines[i] = tf.Pad + l
}
out = "\n" + strings.Join(lines, "\n") + "\n"
}
out = trailingSpaces.ReplaceAllString(out, "")
return out, nil
}
func convert(o *node) (*data, error) {
names := []string{}
curr := o
for curr != nil {
names = append([]string{curr.name}, names...)
curr = curr.parent
}
name := strings.Join(names, " ")
args := make([]*datum, len(o.args))
for i, arg := range o.args {
//arguments are required
n := "<" + arg.name + ">"
//unless...
if arg.slice {
p := []string{arg.name, arg.name}
for i, n := range p {
if i < arg.min {
//still required
n = "<" + n + ">"
} else {
//optional!
n = "[" + n + "]"
}
p[i] = n
}
n = strings.Join(p, " ") + " ..."
}
args[i] = &datum{
Name: n,
Help: constrain(arg.help, o.lineWidth),
}
}
flagGroups := make([]*datumGroup, len(o.flagGroups))
//initialise and calculate padding
max := 0
pad := nletters(' ', o.padWidth)
for i, g := range o.flagGroups {
dg := &datumGroup{
Name: g.name,
Flags: make([]*datum, len(g.flags)),
}
flagGroups[i] = dg
for i, item := range g.flags {
to := &datum{Pad: pad}
to.Name = "--" + item.name
if item.shortName != "" && !o.flagSkipShort[item.name] {
to.Name += ", -" + item.shortName
}
l := len(to.Name)
//max shared across ALL groups
if l > max {
max = l
}
dg.Flags[i] = to
}
}
//get item help, with optional default values and env names and
//constrain to a specific line width
extras := make([]*template.Template, 3)
keys := []string{"default", "env", "multiple"}
for i, k := range keys {
t, err := template.New("").Parse(o.templates["extra"+k])
if err != nil {
return nil, fmt.Errorf("template extra%s: %s", k, err)
}
extras[i] = t
}
//calculate...
padsInOption := o.padWidth
optionNameWidth := max + padsInOption
spaces := nletters(' ', optionNameWidth)
helpWidth := o.lineWidth - optionNameWidth
//go back and render each option using calculated values
for i, dg := range flagGroups {
for j, to := range dg.Flags {
//pad all option names to be the same length
to.Name += spaces[:max-len(to.Name)]
//constrain help text
item := o.flagGroups[i].flags[j]
//render flag help string
vals := []interface{}{item.defstr, item.envName, item.slice}
outs := []string{}
for i, v := range vals {
b := strings.Builder{}
if err := extras[i].Execute(&b, v); err != nil {
return nil, err
}
if b.Len() > 0 {
outs = append(outs, b.String())
}
}
help := item.help
extra := strings.Join(outs, ", ")
if extra != "" {
if help == "" {
help = extra
} else if trailingBrackets.MatchString(help) {
m := trailingBrackets.FindStringSubmatch(help)
help = m[1] + "(" + m[2] + ", " + extra + ")"
} else {
help += " (" + extra + ")"
}
}
help = constrain(help, helpWidth)
//align each row after the flag
lines := strings.Split(help, "\n")
for i, l := range lines {
if i > 0 {
lines[i] = spaces + l
}
}
to.Help = strings.Join(lines, "\n")
}
}
//commands
max = 0
for _, s := range o.cmds {
if l := len(s.name); l > max {
max = l
}
}
subs := make([]*datum, len(o.cmds))
i := 0
cmdNames := []string{}
for _, s := range o.cmds {
cmdNames = append(cmdNames, s.name)
}
sort.Strings(cmdNames)
for _, name := range cmdNames {
s := o.cmds[name]
h := s.help
if h == "" {
h = s.summary
}
explicitMatch := o.cmdname != nil && *o.cmdname == s.name
envMatch := o.cmdnameEnv != "" && os.Getenv(o.cmdnameEnv) == s.name
if explicitMatch || envMatch {
if h == "" {
h = "default"
} else {
h += " (default)"
}
}
subs[i] = &datum{
Name: s.name,
Help: h,
Pad: nletters(' ', max-len(s.name)),
}
i++
}
//convert error to string
err := ""
if o.err != nil {
err = o.err.Error()
}
return &data{
datum: datum{
Name: name,
Help: o.help,
Pad: pad,
},
Args: args,
FlagGroups: flagGroups,
Cmds: subs,
Order: o.order,
Version: o.version,
Summary: constrain(o.summary, o.lineWidth),
Repo: o.repo,
Author: o.author,
ErrMsg: err,
}, nil
}