Merge pull request #1119 from mostynb/word_wrap_v2
Add word-wrap support, with wrap length provided by the user
This commit is contained in:
commit
e576ba4022
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
|
// 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 default implementation of HelpPrinter, and may be called directly if
|
||||||
// the ExtraInfo field is set on an App.
|
// 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
|
var HelpPrinterCustom helpPrinterCustom = printHelpCustom
|
||||||
|
|
||||||
// VersionPrinter prints the version for the App
|
// 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
|
// The customFuncs map will be combined with a default template.FuncMap to
|
||||||
// allow using arbitrary functions in template rendering.
|
// 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{}) {
|
||||||
|
|
||||||
|
const maxLineLength = 10000
|
||||||
|
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"join": strings.Join,
|
"join": strings.Join,
|
||||||
"indent": indent,
|
"indent": indent,
|
||||||
"nindent": nindent,
|
"nindent": nindent,
|
||||||
"trim": strings.TrimSpace,
|
"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 {
|
for key, value := range customFuncs {
|
||||||
funcMap[key] = value
|
funcMap[key] = value
|
||||||
}
|
}
|
||||||
@ -402,3 +424,55 @@ func indent(spaces int, v string) string {
|
|||||||
func nindent(spaces int, v string) string {
|
func nindent(spaces int, v string) string {
|
||||||
return "\n" + indent(spaces, v)
|
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
|
// cli.go uses text/template to render templates. You can
|
||||||
// render custom help text by setting this variable.
|
// render custom help text by setting this variable.
|
||||||
var AppHelpTemplate = `NAME:
|
var AppHelpTemplate = `NAME:
|
||||||
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
|
{{$v := offset .Name 6}}{{wrap .Name 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}
|
||||||
|
|
||||||
USAGE:
|
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:
|
||||||
{{.Version}}{{end}}{{end}}{{if .Description}}
|
{{.Version}}{{end}}{{end}}{{if .Description}}
|
||||||
|
|
||||||
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}}:
|
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
|
||||||
{{range $index, $author := .Authors}}{{if $index}}
|
{{range $index, $author := .Authors}}{{if $index}}
|
||||||
@ -31,26 +31,26 @@ GLOBAL OPTIONS:{{range .VisibleFlagCategories}}
|
|||||||
|
|
||||||
GLOBAL OPTIONS:
|
GLOBAL OPTIONS:
|
||||||
{{range $index, $option := .VisibleFlags}}{{if $index}}
|
{{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:
|
||||||
{{.Copyright}}{{end}}
|
{{wrap .Copyright 3}}{{end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
// CommandHelpTemplate is the text template for the command help topic.
|
// CommandHelpTemplate is the text template for the command help topic.
|
||||||
// cli.go uses text/template to render templates. You can
|
// cli.go uses text/template to render templates. You can
|
||||||
// render custom help text by setting this variable.
|
// render custom help text by setting this variable.
|
||||||
var CommandHelpTemplate = `NAME:
|
var CommandHelpTemplate = `NAME:
|
||||||
{{.HelpName}} - {{.Usage}}
|
{{$v := offset .HelpName 6}}{{wrap .HelpName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}
|
||||||
|
|
||||||
USAGE:
|
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:
|
||||||
{{.Category}}{{end}}{{if .Description}}
|
{{.Category}}{{end}}{{if .Description}}
|
||||||
|
|
||||||
DESCRIPTION:
|
DESCRIPTION:
|
||||||
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}}
|
{{wrap .Description 3}}{{end}}{{if .VisibleFlagCategories}}
|
||||||
|
|
||||||
OPTIONS:{{range .VisibleFlagCategories}}
|
OPTIONS:{{range .VisibleFlagCategories}}
|
||||||
{{if .Name}}{{.Name}}
|
{{if .Name}}{{.Name}}
|
||||||
@ -69,10 +69,10 @@ var SubcommandHelpTemplate = `NAME:
|
|||||||
{{.HelpName}} - {{.Usage}}
|
{{.HelpName}} - {{.Usage}}
|
||||||
|
|
||||||
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:
|
||||||
{{.Description | nindent 3 | trim}}{{end}}
|
{{wrap .Description 3}}{{end}}
|
||||||
|
|
||||||
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||||
{{.Name}}:{{range .VisibleCommands}}
|
{{.Name}}:{{range .VisibleCommands}}
|
||||||
|
Loading…
Reference in New Issue
Block a user