diff --git a/help.go b/help.go index c1e974a481..20e12761f9 100644 --- a/help.go +++ b/help.go @@ -59,6 +59,11 @@ var HelpPrinter helpPrinter = printHelp // HelpPrinterCustom is a function that writes the help output. It is used as // the default implementation of HelpPrinter, and may be called directly if // the ExtraInfo field is set on an App. +// +// In the default implementation, if the customFuncs argument contains a +// "wrapAt" key, which is a function which takes no arguments and returns +// an int, this int value will be used to produce a "wrap" function used +// by the default template to wrap long lines. var HelpPrinterCustom helpPrinterCustom = printHelpCustom // VersionPrinter prints the version for the App @@ -263,8 +268,22 @@ func ShowCommandCompletions(ctx *Context, command string) { // allow using arbitrary functions in template rendering. func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { funcMap := template.FuncMap{ - "join": strings.Join, + "join": strings.Join, + "wrap": func(input string, offset int) string { return input }, + "offset": offset, + } + + if customFuncs["wrapAt"] != nil { + if wa, ok := customFuncs["wrapAt"]; ok { + if waf, ok := wa.(func() int); ok { + wrapAt := waf() + customFuncs["wrap"] = func(input string, offset int) string { + return wrap(input, offset, wrapAt) + } + } + } } + for key, value := range customFuncs { funcMap[key] = value } @@ -366,3 +385,55 @@ 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") + + padding := strings.Repeat(" ", offset) + + for i, line := range lines { + if i != 0 { + sb.WriteString(padding) + } + + sb.WriteString(wrapLine(line, offset, wrapAt, padding)) + + if i != len(lines)-1 { + sb.WriteString("\n") + } + } + + return sb.String() +} + +func wrapLine(input string, offset int, wrapAt int, padding string) string { + if wrapAt <= offset || len(input) <= wrapAt-offset { + return input + } + + lineWidth := wrapAt - offset + words := strings.Fields(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" + padding + 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..6e758f8d30 100644 --- a/help_test.go +++ b/help_test.go @@ -907,3 +907,89 @@ func TestHideHelpCommand_WithSubcommands(t *testing.T) { t.Errorf("Run returned unexpected error: %v", err) } } + +func TestWrappedHelp(t *testing.T) { + + // Reset HelpPrinter after this test. + defer func(old helpPrinter) { + HelpPrinter = old + }(HelpPrinter) + + output := new(bytes.Buffer) + app := &App{ + Writer: output, + 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) + + HelpPrinter = func(w io.Writer, templ string, data interface{}) { + funcMap := map[string]interface{}{ + "wrapAt": func() int { + return 30 + }, + } + + HelpPrinterCustom(w, templ, data, funcMap) + } + + _ = 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, got:\n%s\nexpected: %s", + output.String(), expected) + } +} 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.