diff --git a/app.go b/app.go index 59bf68df95..4b49e9d374 100644 --- a/app.go +++ b/app.go @@ -74,6 +74,8 @@ type App struct { Copyright string // Writer writer to write output to Writer io.Writer + // If greater than zero, wrap help text lines at this length (experimental). + WrapAt int // ErrWriter writes error output ErrWriter io.Writer // ExitErrHandler processes any error encountered while running an App before diff --git a/app_test.go b/app_test.go index 6c95faa612..e024572ecb 100644 --- a/app_test.go +++ b/app_test.go @@ -1197,7 +1197,7 @@ func TestAppHelpPrinter(t *testing.T) { }() var wasCalled = false - HelpPrinter = func(w io.Writer, template string, data interface{}) { + HelpPrinter = func(w io.Writer, template string, data interface{}, wrapAt int) { wasCalled = true } diff --git a/go.mod b/go.mod index c38d41c14b..a5a259ce92 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.11 require ( github.com/BurntSushi/toml v0.3.1 + github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d - gopkg.in/yaml.v2 v2.2.2 + github.com/sergi/go-diff v1.1.0 // indirect + gopkg.in/yaml.v2 v2.2.4 ) diff --git a/go.sum b/go.sum index ef121ff5db..62aeb1d4fe 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,28 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/help.go b/help.go index c1e974a481..d2507217c5 100644 --- a/help.go +++ b/help.go @@ -42,10 +42,10 @@ var helpSubcommand = &Command{ } // Prints help for the App or Command -type helpPrinter func(w io.Writer, templ string, data interface{}) +type helpPrinter func(w io.Writer, templ string, data interface{}, wrapAt int) // Prints help for the App or Command with custom template function. -type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}, wrapAt int) // HelpPrinter is a function that writes the help output. If not set explicitly, // this calls HelpPrinterCustom using only the default template functions. @@ -78,7 +78,7 @@ func ShowAppHelp(c *Context) error { } if c.App.ExtraInfo == nil { - HelpPrinter(c.App.Writer, template, c.App) + HelpPrinter(c.App.Writer, template, c.App, c.App.WrapAt) return nil } @@ -87,7 +87,7 @@ func ShowAppHelp(c *Context) error { "ExtraInfo": c.App.ExtraInfo, } } - HelpPrinterCustom(c.App.Writer, template, c.App, customAppData()) + HelpPrinterCustom(c.App.Writer, template, c.App, customAppData(), c.App.WrapAt) return nil } @@ -189,7 +189,7 @@ func ShowCommandHelpAndExit(c *Context, command string, code int) { func ShowCommandHelp(ctx *Context, command string) error { // show the subcommand help for a command with subcommands if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) + HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App, ctx.App.WrapAt) return nil } @@ -200,7 +200,7 @@ func ShowCommandHelp(ctx *Context, command string) error { templ = CommandHelpTemplate } - HelpPrinter(ctx.App.Writer, templ, c) + HelpPrinter(ctx.App.Writer, templ, c, ctx.App.WrapAt) return nil } @@ -261,9 +261,22 @@ func ShowCommandCompletions(ctx *Context, command string) { // // The customFuncs map will be combined with a default template.FuncMap to // allow using arbitrary functions in template rendering. -func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { +func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}, wrapAt int) { + + wrapFunc := func(input string, offset int) string { + return input + } + + if wrapAt > 0 { + wrapFunc = func(input string, offset int) string { + return wrap(input, offset, wrapAt) + } + } + funcMap := template.FuncMap{ - "join": strings.Join, + "join": strings.Join, + "wrap": wrapFunc, + "offset": offset, } for key, value := range customFuncs { funcMap[key] = value @@ -284,8 +297,8 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs _ = w.Flush() } -func printHelp(out io.Writer, templ string, data interface{}) { - HelpPrinterCustom(out, templ, data, nil) +func printHelp(out io.Writer, templ string, data interface{}, wrapAt int) { + HelpPrinterCustom(out, templ, data, nil, wrapAt) } func checkVersion(c *Context) bool { @@ -366,3 +379,53 @@ func checkCommandCompletions(c *Context, name string) bool { ShowCommandCompletions(c, name) return true } + +func wrap(input string, offset int, wrapAt int) string { + var sb strings.Builder + + lines := strings.Split(input, "\n") + + for i, line := range lines { + if i != 0 { + sb.WriteString(strings.Repeat(" ", offset)) + } + + sb.WriteString(wrapLine(line, offset, wrapAt)) + + if i != len(lines)-1 { + sb.WriteString("\n") + } + } + + return sb.String() +} + +func wrapLine(input string, offset int, wrapAt int) string { + if wrapAt <= offset || len(input) <= wrapAt-offset { + return input + } + + lineWidth := wrapAt - offset + words := strings.Fields(strings.TrimSpace(input)) + if len(words) == 0 { + return input + } + + wrapped := words[0] + spaceLeft := lineWidth - len(wrapped) + for _, word := range words[1:] { + if len(word)+1 > spaceLeft { + wrapped += "\n" + strings.Repeat(" ", offset) + word + spaceLeft = lineWidth - len(word) + } else { + wrapped += " " + word + spaceLeft -= 1 + len(word) + } + } + + return wrapped +} + +func offset(input string, fixed int) int { + return len(input) + fixed +} diff --git a/help_test.go b/help_test.go index 5f292b77e5..eca05f8295 100644 --- a/help_test.go +++ b/help_test.go @@ -9,6 +9,8 @@ import ( "runtime" "strings" "testing" + + "github.com/andreyvit/diff" ) func Test_ShowAppHelp_NoAuthor(t *testing.T) { @@ -227,7 +229,7 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { { name: "no-command", template: "", - printer: func(w io.Writer, templ string, data interface{}) { + printer: func(w io.Writer, templ string, data interface{}, wrapAt int) { fmt.Fprint(w, "yo") }, command: "", @@ -237,7 +239,7 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { { name: "standard-command", template: "", - printer: func(w io.Writer, templ string, data interface{}) { + printer: func(w io.Writer, templ string, data interface{}, wrapAt int) { fmt.Fprint(w, "yo") }, command: "my-command", @@ -247,10 +249,10 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { { name: "custom-template-command", template: "{{doublecho .Name}}", - printer: func(w io.Writer, templ string, data interface{}) { + printer: func(w io.Writer, templ string, data interface{}, wrapAt int) { // Pass a custom function to ensure it gets used fm := map[string]interface{}{"doublecho": doublecho} - HelpPrinterCustom(w, templ, data, fm) + HelpPrinterCustom(w, templ, data, fm, wrapAt) }, command: "my-command", wantTemplate: "{{doublecho .Name}}", @@ -263,12 +265,12 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { defer func(old helpPrinter) { HelpPrinter = old }(HelpPrinter) - HelpPrinter = func(w io.Writer, templ string, data interface{}) { + HelpPrinter = func(w io.Writer, templ string, data interface{}, wrapAt int) { if templ != tt.wantTemplate { t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) } - tt.printer(w, templ, data) + tt.printer(w, templ, data, wrapAt) } var buf bytes.Buffer @@ -312,7 +314,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { { name: "no-command", template: "", - printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}, wrapAt int) { fmt.Fprint(w, "yo") }, command: "", @@ -322,7 +324,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { { name: "standard-command", template: "", - printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}, wrapAt int) { fmt.Fprint(w, "yo") }, command: "my-command", @@ -332,10 +334,10 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { { name: "custom-template-command", template: "{{doublecho .Name}}", - printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}) { + printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}, wrapAt int) { // Pass a custom function to ensure it gets used fm := map[string]interface{}{"doublecho": doublecho} - printHelpCustom(w, templ, data, fm) + printHelpCustom(w, templ, data, fm, wrapAt) }, command: "my-command", wantTemplate: "{{doublecho .Name}}", @@ -348,7 +350,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { defer func(old helpPrinterCustom) { HelpPrinterCustom = old }(HelpPrinterCustom) - HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}, wrapAt int) { if fm != nil { t.Error("unexpected function map passed") } @@ -357,7 +359,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) } - tt.printer(w, templ, data, fm) + tt.printer(w, templ, data, fm, wrapAt) } var buf bytes.Buffer @@ -566,7 +568,7 @@ func TestShowAppHelp_HelpPrinter(t *testing.T) { { name: "standard-command", template: "", - printer: func(w io.Writer, templ string, data interface{}) { + printer: func(w io.Writer, templ string, data interface{}, wrapAt int) { fmt.Fprint(w, "yo") }, wantTemplate: AppHelpTemplate, @@ -575,10 +577,10 @@ func TestShowAppHelp_HelpPrinter(t *testing.T) { { name: "custom-template-command", template: "{{doublecho .Name}}", - printer: func(w io.Writer, templ string, data interface{}) { + printer: func(w io.Writer, templ string, data interface{}, wrapAt int) { // Pass a custom function to ensure it gets used fm := map[string]interface{}{"doublecho": doublecho} - printHelpCustom(w, templ, data, fm) + printHelpCustom(w, templ, data, fm, wrapAt) }, wantTemplate: "{{doublecho .Name}}", wantOutput: "my-app my-app", @@ -590,12 +592,12 @@ func TestShowAppHelp_HelpPrinter(t *testing.T) { defer func(old helpPrinter) { HelpPrinter = old }(HelpPrinter) - HelpPrinter = func(w io.Writer, templ string, data interface{}) { + HelpPrinter = func(w io.Writer, templ string, data interface{}, wrapAt int) { if templ != tt.wantTemplate { t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) } - tt.printer(w, templ, data) + tt.printer(w, templ, data, wrapAt) } var buf bytes.Buffer @@ -633,7 +635,7 @@ func TestShowAppHelp_HelpPrinterCustom(t *testing.T) { { name: "standard-command", template: "", - printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}, wrapAt int) { fmt.Fprint(w, "yo") }, wantTemplate: AppHelpTemplate, @@ -642,10 +644,10 @@ func TestShowAppHelp_HelpPrinterCustom(t *testing.T) { { name: "custom-template-command", template: "{{doublecho .Name}}", - printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}) { + printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}, wrapAt int) { // Pass a custom function to ensure it gets used fm := map[string]interface{}{"doublecho": doublecho} - printHelpCustom(w, templ, data, fm) + printHelpCustom(w, templ, data, fm, wrapAt) }, wantTemplate: "{{doublecho .Name}}", wantOutput: "my-app my-app", @@ -657,7 +659,7 @@ func TestShowAppHelp_HelpPrinterCustom(t *testing.T) { defer func(old helpPrinterCustom) { HelpPrinterCustom = old }(HelpPrinterCustom) - HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}, wrapAt int) { if fm != nil { t.Error("unexpected function map passed") } @@ -666,7 +668,7 @@ func TestShowAppHelp_HelpPrinterCustom(t *testing.T) { t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) } - tt.printer(w, templ, data, fm) + tt.printer(w, templ, data, fm, wrapAt) } var buf bytes.Buffer @@ -907,3 +909,74 @@ func TestHideHelpCommand_WithSubcommands(t *testing.T) { t.Errorf("Run returned unexpected error: %v", err) } } + +func TestWrappedHelp(t *testing.T) { + output := new(bytes.Buffer) + app := &App{ + Writer: output, + WrapAt: 30, + Flags: []Flag{ + &BoolFlag{Name: "foo", + Aliases: []string{"h"}, + Usage: "here's a really long help text line, let's see where it wraps. blah blah blah and so on.", + }, + }, + Usage: "here's a sample App.Usage string long enough that it should be wrapped in this test", + UsageText: "i'm not sure how App.UsageText differs from App.Usage, but this should also be wrapped in this test", + // TODO: figure out how to make ArgsUsage appear in the help text, and test that + Description: "here's a sample App.Description string long enough that it should be wrapped in this test", + Copyright: `Here's a sample copyright text string long enough that it should be wrapped. +Including newlines. + + +And then another long line. Blah blah blah does anybody ever read these things?`, + } + + c := NewContext(app, nil, nil) + + _ = ShowAppHelp(c) + + expected := `NAME: + - here's a sample + App.Usage string long + enough that it should be + wrapped in this test + +USAGE: + i'm not sure how + App.UsageText differs from + App.Usage, but this should + also be wrapped in this + test + +DESCRIPTION: + here's a sample + App.Description string long + enough that it should be + wrapped in this test + +GLOBAL OPTIONS: + --foo, -h here's a + really long help text + line, let's see where it + wraps. blah blah blah + and so on. (default: + false) + +COPYRIGHT: + Here's a sample copyright + text string long enough + that it should be wrapped. + Including newlines. + + + And then another long line. + Blah blah blah does anybody + ever read these things? +` + + if output.String() != expected { + t.Errorf("Unexpected wrapping, diff:\n%s", + diff.LineDiff(expected, output.String())) + } +} diff --git a/template.go b/template.go index aee3e0494f..8c97e1accb 100644 --- a/template.go +++ b/template.go @@ -4,16 +4,16 @@ package cli // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + {{$v := offset .Name 6}}{{wrap .Name 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} + {{wrap .Description 3}}{{end}}{{if len .Authors}} AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} @@ -26,10 +26,10 @@ COMMANDS:{{range .VisibleCategories}}{{if .Name}} GLOBAL OPTIONS: {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + {{end}}{{wrap $option.String 6}}{{end}}{{end}}{{if .Copyright}} COPYRIGHT: - {{.Copyright}}{{end}} + {{wrap .Copyright 3}}{{end}} ` // CommandHelpTemplate is the text template for the command help topic.