Repository: urfave/cli
Pull request #1460 (2022-08-20 22:14:22)

Fix:(issue_1293) Wrap usage text for commands
<!--
  This template provides some ideas of things to include in your PR description.
  To start, try providing a short summary of your changes in the Title above.
  If a section of the PR template does not apply to this PR, then delete that section.
 -->

## What type of PR is this?

_(REQUIRED)_

<!--
  Delete any of the following that do not apply:
 -->

- bug

## What this PR does / why we need it:

_(REQUIRED)_

<!--
  What goal is this change working towards?
  Provide a bullet pointed summary of how each file was changed.
  Briefly explain any decisions you made with respect to the changes.
  Include anything here that you didn't include in *Release Notes*
  above, such as changes to CI or changes to internal methods.
-->

Fix command usage wrap for multiline text
## Which issue(s) this PR fixes:

_(REQUIRED)_

<!--
If this PR fixes one of more issues, list them here.
One line each, like so:

Fixes #1293 
-->

## Special notes for your reviewer:

_(fill-in or delete this section)_

<!--
   Is there any particular feedback you would / wouldn't like?
   Which parts of the code should reviewers focus on?
-->

## Testing

_(fill-in or delete this section)_

<!--
  Describe how you tested this change.
-->

go test -run=TestShowAppHelp_CommandMultiLine_UsageText

## Release Notes

_(REQUIRED)_
<!--
  If this PR makes user facing changes, please describe them here. This
  description will be copied into the release notes/changelog, whenever the
  next version is released. Keep this section short, and focus on high level
  changes.

  Put your text between the block. To omit notes, use NONE within the block.
-->

```release-note

```


WestleyK: Thanks for looking into this, so heres the output with your changes here:

```
$ ./foo 
NAME:
   foo - sample usage

USAGE:
   foo [global options] command [command options] [arguments...]

COMMANDS:
   test  long help for test command
   foo   long help for foo command
        this command does nothing.
   run  this is a long help output for the run command, long usage
        output, long usage output, long usage output, long usage output
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)
```

Better, but the indenting is not consistent with the other commands, heres what I would expect it to look like:

```
$ ./foo 
NAME:
   foo - sample usage

USAGE:
   foo [global options] command [command options] [arguments...]

COMMANDS:
   test     long help for test command
   foo      long help for foo command
            this command does nothing.
   run      this is a long help output for the run command, long usage
            output, long usage output, long usage output, long usage output
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)
```

Heres what it looks like without any newlines (before your changes):

```
$ ./foo 
NAME:
   foo - sample usage

USAGE:
   foo [global options] command [command options] [arguments...]

COMMANDS:
   test     long help for test command
   foo      long help for foo command this command does nothing.
   run      this is a long help output for the run command, long usage output, long usage output, long usage output, long usage output
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)
```
dearchap: @WestleyK Give it a shot now. 
WestleyK: Looks perfect! Thanks for taking care of this issue!

```
$ ./foo 
NAME:
   foo - sample usage

USAGE:
   foo [global options] command [command options] [arguments...]

COMMANDS:
   test     long help for test command
   foo      long help for foo command
            this command does nothing.
   run      this is a long help output for the run command, long usage
            output, long usage output, long usage output, long usage output
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)
```

<details><summary>Code used</summary>
<p>

```go
package main

import (
	"fmt"
	"os"

	"github.com/urfave/cli/v2"
)

func main() {
	app := &cli.App{
		Usage: "sample usage",
		Commands: []*cli.Command{
			{
				Name:  "test",
				Usage: "long help for test command",
			},
			{
				Name:  "foo",
				Usage: "long help for foo command\nthis command does nothing.",
			},
			{
				Name:  "run",
				Usage: "this is a long help output for the run command, long usage\noutput, long usage output, long usage output, long usage output",
				Action: func(c *cli.Context) error {
					return fmt.Errorf("nothing")
				},
			},
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		fmt.Fprintf(os.Stdout, "%s: %s\n", os.Args[0], err)
		os.Exit(1)
	}
}
```

go.mod
```go
module foo

go 1.18

require github.com/urfave/cli/v2 v2.11.2

require (
	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
)

replace github.com/urfave/cli/v2 => github.com/dearchap/cli/v2 v2.3.1-0.20220824131205-915fd7adcaa5
```

</p>
</details>

Should also update the documentation that `Usage` allows newlines.
dearchap: @WestleyK would you like to submit a PR for the documentation ? 

Patch:
diff --git a/docs/index.md b/docs/index.md
index 251679d..60a1102 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -11,7 +11,7 @@ an expressive way.
 
 These are the guides for each major supported version:
 
-- [`v2`](./v2/)
+- [`v2`](./v2/getting-started)
 - [`v1`](./v1/getting-started)
 
 In addition to the version-specific guides, these other documents are available:
diff --git a/help.go b/help.go
index 9a8d243..d6caea4 100644
--- a/help.go
+++ b/help.go
@@ -295,12 +295,14 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs
 	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,
+		"join":           strings.Join,
+		"subtract":       subtract,
+		"indent":         indent,
+		"nindent":        nindent,
+		"trim":           strings.TrimSpace,
+		"wrap":           func(input string, offset int) string { return wrap(input, offset, maxLineLength) },
+		"offset":         offset,
+		"offsetCommands": offsetCommands,
 	}
 
 	if customFuncs["wrapAt"] != nil {
@@ -416,6 +418,10 @@ func checkCommandCompletions(c *Context, name string) bool {
 	return true
 }
 
+func subtract(a, b int) int {
+	return a - b
+}
+
 func indent(spaces int, v string) string {
 	pad := strings.Repeat(" ", spaces)
 	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
@@ -476,3 +482,28 @@ func wrapLine(input string, offset int, wrapAt int, padding string) string {
 func offset(input string, fixed int) int {
 	return len(input) + fixed
 }
+
+// this function tries to find the max width of the names column
+// so say we have the following rows for help
+//
+//	foo1, foo2, foo3  some string here
+//	bar1, b2 some other string here
+//
+// We want to offset the 2nd row usage by some amount so that everything
+// is aligned
+//
+//	foo1, foo2, foo3  some string here
+//	bar1, b2          some other string here
+//
+// to find that offset we find the length of all the rows and use the max
+// to calculate the offset
+func offsetCommands(cmds []*Command, fixed int) int {
+	var max int = 0
+	for _, cmd := range cmds {
+		s := strings.Join(cmd.Names(), ", ")
+		if len(s) > max {
+			max = len(s)
+		}
+	}
+	return max + fixed
+}
diff --git a/template.go b/template.go
index f3116fd..7ed2370 100644
--- a/template.go
+++ b/template.go
@@ -21,8 +21,8 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
 
 COMMANDS:{{range .VisibleCategories}}{{if .Name}}
    {{.Name}}:{{range .VisibleCommands}}
-     {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
-   {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}}
+     {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}}
+   {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}}
 
 GLOBAL OPTIONS:{{range .VisibleFlagCategories}}
    {{if .Name}}{{.Name}}
@@ -76,8 +76,8 @@ DESCRIPTION:
 
 COMMANDS:{{range .VisibleCategories}}{{if .Name}}
    {{.Name}}:{{range .VisibleCommands}}
-     {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
-   {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
+     {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}}
+     {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
 
 OPTIONS:
    {{range .VisibleFlags}}{{.}}


Test patch:
diff --git a/help_test.go b/help_test.go
index 4feb7f0..9422b4a 100644
--- a/help_test.go
+++ b/help_test.go
@@ -895,6 +895,42 @@ App UsageText`,
 	}
 }
 
+func TestShowAppHelp_CommandMultiLine_UsageText(t *testing.T) {
+	app := &App{
+		UsageText: `This is a
+multi
+line
+App UsageText`,
+		Commands: []*Command{
+			{
+				Name:    "frobbly",
+				Aliases: []string{"frb1", "frbb2", "frl2"},
+				Usage:   "this is a long help output for the run command, long usage \noutput, long usage output, long usage output, long usage output\noutput, long usage output, long usage output",
+			},
+			{
+				Name:    "grobbly",
+				Aliases: []string{"grb1", "grbb2"},
+				Usage:   "this is another long help output for the run command, long usage \noutput, long usage output",
+			},
+		},
+	}
+
+	output := &bytes.Buffer{}
+	app.Writer = output
+
+	_ = app.Run([]string{"foo"})
+
+	expected := "COMMANDS:\n" +
+		"   frobbly, frb1, frbb2, frl2  this is a long help output for the run command, long usage \n" +
+		"                               output, long usage output, long usage output, long usage output\n" +
+		"                               output, long usage output, long usage output\n" +
+		"   grobbly, grb1, grbb2        this is another long help output for the run command, long usage \n" +
+		"                               output, long usage output"
+	if !strings.Contains(output.String(), expected) {
+		t.Errorf("expected output to include usage text; got: %q", output.String())
+	}
+}
+
 func TestHideHelpCommand(t *testing.T) {
 	app := &App{
 		HideHelpCommand: true,