Add word-wrap support, with wrap length provided by the user
We could try to automatically detect the terminal width and wrap at that point, but this would increase the binary footprint for all users even if not using this feature. Instead, we can allow users to specify their preferred line length limit (if any), and those who want to bear the cost of checking the terminal size can do so if they wish. This also makes the feature more testable. Original patch by Sascha Grunert <sgrunert@suse.com>
This commit is contained in:
parent
595cabc60c
commit
4fd56cb22d
74
help.go
74
help.go
@ -64,6 +64,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
|
||||
@ -286,12 +291,29 @@ 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{}) {
|
||||
|
||||
const maxLineLength = 10000
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"join": strings.Join,
|
||||
"indent": indent,
|
||||
"nindent": nindent,
|
||||
"trim": strings.TrimSpace,
|
||||
"wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) },
|
||||
"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
|
||||
}
|
||||
@ -402,3 +424,55 @@ func indent(spaces int, v string) string {
|
||||
func nindent(spaces int, v string) string {
|
||||
return "\n" + indent(spaces, v)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
222
help_test.go
222
help_test.go
@ -1124,3 +1124,225 @@ func TestDefaultCompleteWithFlags(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
with a newline
|
||||
and an indented line`,
|
||||
Copyright: `Here's a sample copyright text string long enough that it should be wrapped.
|
||||
Including newlines.
|
||||
And also indented lines.
|
||||
|
||||
|
||||
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
|
||||
|
||||
with a newline
|
||||
and an indented line
|
||||
|
||||
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 also indented lines.
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedCommandHelp(t *testing.T) {
|
||||
|
||||
// Reset HelpPrinter after this test.
|
||||
defer func(old helpPrinter) {
|
||||
HelpPrinter = old
|
||||
}(HelpPrinter)
|
||||
|
||||
output := new(bytes.Buffer)
|
||||
app := &App{
|
||||
Writer: output,
|
||||
Commands: []*Command{
|
||||
{
|
||||
Name: "add",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "add a task to the list",
|
||||
UsageText: "this is an even longer way of describing adding a task to the list",
|
||||
Description: "and a description long enough to wrap in this test case",
|
||||
Action: func(c *Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
_ = ShowCommandHelp(c, "add")
|
||||
|
||||
expected := `NAME:
|
||||
- add a task to the list
|
||||
|
||||
USAGE:
|
||||
this is an even longer way
|
||||
of describing adding a task
|
||||
to the list
|
||||
|
||||
DESCRIPTION:
|
||||
and a description long
|
||||
enough to wrap in this test
|
||||
case
|
||||
`
|
||||
|
||||
if output.String() != expected {
|
||||
t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s",
|
||||
output.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedSubcommandHelp(t *testing.T) {
|
||||
|
||||
// Reset HelpPrinter after this test.
|
||||
defer func(old helpPrinter) {
|
||||
HelpPrinter = old
|
||||
}(HelpPrinter)
|
||||
|
||||
output := new(bytes.Buffer)
|
||||
app := &App{
|
||||
Name: "cli.test",
|
||||
Writer: output,
|
||||
Commands: []*Command{
|
||||
{
|
||||
Name: "bar",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "add a task to the list",
|
||||
UsageText: "this is an even longer way of describing adding a task to the list",
|
||||
Description: "and a description long enough to wrap in this test case",
|
||||
Action: func(c *Context) error {
|
||||
return nil
|
||||
},
|
||||
Subcommands: []*Command{
|
||||
{
|
||||
Name: "grok",
|
||||
Usage: "remove an existing template",
|
||||
UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more",
|
||||
Action: func(c *Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
HelpPrinter = func(w io.Writer, templ string, data interface{}) {
|
||||
funcMap := map[string]interface{}{
|
||||
"wrapAt": func() int {
|
||||
return 30
|
||||
},
|
||||
}
|
||||
|
||||
HelpPrinterCustom(w, templ, data, funcMap)
|
||||
}
|
||||
|
||||
_ = app.Run([]string{"foo", "bar", "grok", "--help"})
|
||||
|
||||
expected := `NAME:
|
||||
cli.test bar grok - remove
|
||||
an
|
||||
existing
|
||||
template
|
||||
|
||||
USAGE:
|
||||
longer usage text goes
|
||||
here, la la la, hopefully
|
||||
this is long enough to wrap
|
||||
even more
|
||||
|
||||
OPTIONS:
|
||||
--help, -h show help (default: false)
|
||||
|
||||
`
|
||||
|
||||
if output.String() != expected {
|
||||
t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s",
|
||||
output.String(), expected)
|
||||
}
|
||||
}
|
||||
|
20
template.go
20
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 | nindent 3 | trim}}{{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 | nindent 3 | trim}}{{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}}
|
||||
@ -31,26 +31,26 @@ GLOBAL OPTIONS:{{range .VisibleFlagCategories}}
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
{{range $index, $option := .VisibleFlags}}{{if $index}}
|
||||
{{end}}{{$option}}{{end}}{{end}}{{end}}{{if .Copyright}}
|
||||
{{end}}{{wrap $option.String 6}}{{end}}{{end}}{{end}}{{if .Copyright}}
|
||||
|
||||
COPYRIGHT:
|
||||
{{.Copyright}}{{end}}
|
||||
{{wrap .Copyright 3}}{{end}}
|
||||
`
|
||||
|
||||
// CommandHelpTemplate is the text template for the command help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var CommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
{{$v := offset .HelpName 6}}{{wrap .HelpName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}
|
||||
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
|
||||
CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}}
|
||||
{{wrap .Description 3}}{{end}}{{if .VisibleFlagCategories}}
|
||||
|
||||
OPTIONS:{{range .VisibleFlagCategories}}
|
||||
{{if .Name}}{{.Name}}
|
||||
@ -69,10 +69,10 @@ var SubcommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
|
||||
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description | nindent 3 | trim}}{{end}}
|
||||
{{wrap .Description 3}}{{end}}
|
||||
|
||||
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||
{{.Name}}:{{range .VisibleCommands}}
|
||||
|
Loading…
Reference in New Issue
Block a user