Skip to content

Commit 70ca134

Browse files
Stebalienhannahhoward
authored andcommitted
wrap synopsys, flag, and argument help text
We could be fancier and figure out the terminal width but wrapping at a fixed 80c is a decent start.
1 parent 31141c7 commit 70ca134

File tree

2 files changed

+62
-19
lines changed

2 files changed

+62
-19
lines changed

cli/helptext.go

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import (
1212
)
1313

1414
const (
15-
requiredArg = "<%v>"
16-
optionalArg = "[<%v>]"
17-
variadicArg = "%v..."
18-
shortFlag = "-%v"
19-
longFlag = "--%v"
20-
optionType = "(%v)"
15+
terminalWidth = 80
16+
requiredArg = "<%v>"
17+
optionalArg = "[<%v>]"
18+
variadicArg = "%v..."
19+
shortFlag = "-%v"
20+
longFlag = "--%v"
21+
optionType = "(%v)"
2122

2223
whitespace = "\r\n\t "
2324

@@ -165,22 +166,24 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
165166
MoreHelp: (cmd != root),
166167
}
167168

169+
width := terminalWidth - len(indentStr)
170+
168171
if len(cmd.Helptext.LongDescription) > 0 {
169172
fields.Description = cmd.Helptext.LongDescription
170173
}
171174

172175
// autogen fields that are empty
173176
if len(fields.Arguments) == 0 {
174-
fields.Arguments = strings.Join(argumentText(cmd), "\n")
177+
fields.Arguments = strings.Join(argumentText(width, cmd), "\n")
175178
}
176179
if len(fields.Options) == 0 {
177-
fields.Options = strings.Join(optionText(cmd), "\n")
180+
fields.Options = strings.Join(optionText(width, cmd), "\n")
178181
}
179182
if len(fields.Subcommands) == 0 {
180183
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
181184
}
182185
if len(fields.Synopsis) == 0 {
183-
fields.Synopsis = generateSynopsis(cmd, pathStr)
186+
fields.Synopsis = generateSynopsis(width, cmd, pathStr)
184187
}
185188

186189
// trim the extra newlines (see TrimNewlines doc)
@@ -221,12 +224,14 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
221224
MoreHelp: (cmd != root),
222225
}
223226

227+
width := terminalWidth - len(indentStr)
228+
224229
// autogen fields that are empty
225230
if len(fields.Subcommands) == 0 {
226231
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
227232
}
228233
if len(fields.Synopsis) == 0 {
229-
fields.Synopsis = generateSynopsis(cmd, pathStr)
234+
fields.Synopsis = generateSynopsis(width, cmd, pathStr)
230235
}
231236

232237
// trim the extra newlines (see TrimNewlines doc)
@@ -238,8 +243,17 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
238243
return shortHelpTemplate.Execute(out, fields)
239244
}
240245

241-
func generateSynopsis(cmd *cmds.Command, path string) string {
246+
func generateSynopsis(width int, cmd *cmds.Command, path string) string {
242247
res := path
248+
currentLineLength := len(res)
249+
appendText := func(text string) {
250+
if currentLineLength+len(text)+1 > width {
251+
res += "\n" + strings.Repeat(" ", len(path))
252+
currentLineLength = len(path)
253+
}
254+
currentLineLength += len(text) + 1
255+
res += " " + text
256+
}
243257
for _, opt := range cmd.Options {
244258
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Name()]
245259
if !ok {
@@ -267,10 +281,10 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
267281
}
268282
}
269283
}
270-
res = fmt.Sprintf("%s [%s]", res, sopt)
284+
appendText("[" + sopt + "]")
271285
}
272286
if len(cmd.Arguments) > 0 {
273-
res = fmt.Sprintf("%s [--]", res)
287+
appendText("[--]")
274288
}
275289
for _, arg := range cmd.Arguments {
276290
sarg := fmt.Sprintf("<%s>", arg.Name)
@@ -281,25 +295,53 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
281295
if !arg.Required {
282296
sarg = fmt.Sprintf("[%s]", sarg)
283297
}
284-
res = fmt.Sprintf("%s %s", res, sarg)
298+
appendText(sarg)
285299
}
286300
return strings.Trim(res, " ")
287301
}
288302

289-
func argumentText(cmd *cmds.Command) []string {
303+
func argumentText(width int, cmd *cmds.Command) []string {
290304
lines := make([]string, len(cmd.Arguments))
291305

292306
for i, arg := range cmd.Arguments {
293307
lines[i] = argUsageText(arg)
294308
}
295309
lines = align(lines)
296310
for i, arg := range cmd.Arguments {
297-
lines[i] += " - " + arg.Description
311+
lines[i] += " - "
312+
lines[i] = appendWrapped(lines[i], arg.Description, width)
298313
}
299314

300315
return lines
301316
}
302317

318+
func appendWrapped(prefix, text string, width int) string {
319+
offset := len(prefix)
320+
bWidth := width - offset
321+
322+
text = strings.Trim(text, whitespace)
323+
// Let the terminal handle wrapping if it's to small. Otherwise
324+
// we get really small lines.
325+
if bWidth < 40 {
326+
prefix += text
327+
return prefix
328+
}
329+
330+
for len(text) > bWidth {
331+
idx := strings.LastIndexAny(text[:bWidth], whitespace)
332+
if idx < 0 {
333+
idx = strings.IndexAny(text, whitespace)
334+
}
335+
if idx < 0 {
336+
break
337+
}
338+
prefix += text[:idx] + "\n" + strings.Repeat(" ", offset)
339+
text = strings.TrimLeft(text[idx:], whitespace)
340+
}
341+
prefix += text
342+
return prefix
343+
}
344+
303345
func optionFlag(flag string) string {
304346
if len(flag) == 1 {
305347
return fmt.Sprintf(shortFlag, flag)
@@ -308,7 +350,7 @@ func optionFlag(flag string) string {
308350
}
309351
}
310352

311-
func optionText(cmd ...*cmds.Command) []string {
353+
func optionText(width int, cmd ...*cmds.Command) []string {
312354
// get a slice of the options we want to list out
313355
options := make([]cmds.Option, 0)
314356
for _, c := range cmd {
@@ -336,7 +378,8 @@ func optionText(cmd ...*cmds.Command) []string {
336378

337379
// add option descriptions to output
338380
for i, opt := range options {
339-
lines[i] += " - " + opt.Description()
381+
lines[i] += " - "
382+
lines[i] = appendWrapped(lines[i], opt.Description(), width)
340383
}
341384

342385
return lines

cli/helptext_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestSynopsisGenerator(t *testing.T) {
2222
},
2323
},
2424
}
25-
syn := generateSynopsis(command, "cmd")
25+
syn := generateSynopsis(terminalWidth, command, "cmd")
2626
t.Logf("Synopsis is: %s", syn)
2727
if !strings.HasPrefix(syn, "cmd ") {
2828
t.Fatal("Synopsis should start with command name")

0 commit comments

Comments
 (0)