Unverified Commit 1995f5cb authored by Steven Allen's avatar Steven Allen Committed by GitHub

Merge pull request #140 from ipfs/feat/wrap-text

improve help text on narrow terminals
parents fa07c339 6a9e61fa
...@@ -4,20 +4,23 @@ import ( ...@@ -4,20 +4,23 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"sort" "sort"
"strings" "strings"
"text/template" "text/template"
"github.com/ipfs/go-ipfs-cmds" cmds "github.com/ipfs/go-ipfs-cmds"
"golang.org/x/crypto/ssh/terminal"
) )
const ( const (
requiredArg = "<%v>" defaultTerminalWidth = 80
optionalArg = "[<%v>]" requiredArg = "<%v>"
variadicArg = "%v..." optionalArg = "[<%v>]"
shortFlag = "-%v" variadicArg = "%v..."
longFlag = "--%v" shortFlag = "-%v"
optionType = "(%v)" longFlag = "--%v"
optionType = "(%v)"
whitespace = "\r\n\t " whitespace = "\r\n\t "
...@@ -28,7 +31,6 @@ type helpFields struct { ...@@ -28,7 +31,6 @@ type helpFields struct {
Indent string Indent string
Usage string Usage string
Path string Path string
ArgUsage string
Tagline string Tagline string
Arguments string Arguments string
Options string Options string
...@@ -48,7 +50,7 @@ type helpFields struct { ...@@ -48,7 +50,7 @@ type helpFields struct {
// ` // `
func (f *helpFields) TrimNewlines() { func (f *helpFields) TrimNewlines() {
f.Path = strings.Trim(f.Path, "\n") f.Path = strings.Trim(f.Path, "\n")
f.ArgUsage = strings.Trim(f.ArgUsage, "\n") f.Usage = strings.Trim(f.Usage, "\n")
f.Tagline = strings.Trim(f.Tagline, "\n") f.Tagline = strings.Trim(f.Tagline, "\n")
f.Arguments = strings.Trim(f.Arguments, "\n") f.Arguments = strings.Trim(f.Arguments, "\n")
f.Options = strings.Trim(f.Options, "\n") f.Options = strings.Trim(f.Options, "\n")
...@@ -66,6 +68,7 @@ func (f *helpFields) IndentAll() { ...@@ -66,6 +68,7 @@ func (f *helpFields) IndentAll() {
return indentString(s, indentStr) return indentString(s, indentStr)
} }
f.Usage = indent(f.Usage)
f.Arguments = indent(f.Arguments) f.Arguments = indent(f.Arguments)
f.Options = indent(f.Options) f.Options = indent(f.Options)
f.Synopsis = indent(f.Synopsis) f.Synopsis = indent(f.Synopsis)
...@@ -73,10 +76,8 @@ func (f *helpFields) IndentAll() { ...@@ -73,10 +76,8 @@ func (f *helpFields) IndentAll() {
f.Description = indent(f.Description) f.Description = indent(f.Description)
} }
const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
const longHelpFormat = `USAGE const longHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}} {{.Usage}}
{{if .Synopsis}}SYNOPSIS {{if .Synopsis}}SYNOPSIS
{{.Synopsis}} {{.Synopsis}}
...@@ -96,11 +97,12 @@ const longHelpFormat = `USAGE ...@@ -96,11 +97,12 @@ const longHelpFormat = `USAGE
{{end}}{{if .Subcommands}}SUBCOMMANDS {{end}}{{if .Subcommands}}SUBCOMMANDS
{{.Subcommands}} {{.Subcommands}}
{{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command. {{.Indent}}For more information about each command, use:
{{.Indent}}'{{.Path}} <subcmd> --help'
{{end}} {{end}}
` `
const shortHelpFormat = `USAGE const shortHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}} {{.Usage}}
{{if .Synopsis}} {{if .Synopsis}}
{{.Synopsis}} {{.Synopsis}}
{{end}}{{if .Description}} {{end}}{{if .Description}}
...@@ -109,18 +111,30 @@ const shortHelpFormat = `USAGE ...@@ -109,18 +111,30 @@ const shortHelpFormat = `USAGE
SUBCOMMANDS SUBCOMMANDS
{{.Subcommands}} {{.Subcommands}}
{{end}}{{if .MoreHelp}} {{end}}{{if .MoreHelp}}
Use '{{.Path}} --help' for more information about this command. {{.Indent}}For more information about each command, use:
{{.Indent}}'{{.Path}} <subcmd> --help'
{{end}} {{end}}
` `
var usageTemplate *template.Template
var longHelpTemplate *template.Template var longHelpTemplate *template.Template
var shortHelpTemplate *template.Template var shortHelpTemplate *template.Template
func getTerminalWidth(out io.Writer) int {
file, ok := out.(*os.File)
if ok {
if terminal.IsTerminal(int(file.Fd())) {
width, _, err := terminal.GetSize(int(file.Fd()))
if err == nil {
return width
}
}
}
return defaultTerminalWidth
}
func init() { func init() {
usageTemplate = template.Must(template.New("usage").Parse(usageFormat)) longHelpTemplate = template.Must(template.New("longHelp").Parse(longHelpFormat))
longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat)) shortHelpTemplate = template.Must(template.New("shortHelp").Parse(shortHelpFormat))
shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
} }
var ErrNoHelpRequested = errors.New("no help requested") var ErrNoHelpRequested = errors.New("no help requested")
...@@ -154,7 +168,6 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) ...@@ -154,7 +168,6 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
fields := helpFields{ fields := helpFields{
Indent: indentStr, Indent: indentStr,
Path: pathStr, Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline, Tagline: cmd.Helptext.Tagline,
Arguments: cmd.Helptext.Arguments, Arguments: cmd.Helptext.Arguments,
Options: cmd.Helptext.Options, Options: cmd.Helptext.Options,
...@@ -165,22 +178,29 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) ...@@ -165,22 +178,29 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
MoreHelp: (cmd != root), MoreHelp: (cmd != root),
} }
width := getTerminalWidth(out) - len(indentStr)
if len(cmd.Helptext.LongDescription) > 0 { if len(cmd.Helptext.LongDescription) > 0 {
fields.Description = cmd.Helptext.LongDescription fields.Description = cmd.Helptext.LongDescription
} }
// autogen fields that are empty // autogen fields that are empty
if len(cmd.Helptext.Usage) > 0 {
fields.Usage = cmd.Helptext.Usage
} else {
fields.Usage = commandUsageText(width, cmd, rootName, path)
}
if len(fields.Arguments) == 0 { if len(fields.Arguments) == 0 {
fields.Arguments = strings.Join(argumentText(cmd), "\n") fields.Arguments = strings.Join(argumentText(width, cmd), "\n")
} }
if len(fields.Options) == 0 { if len(fields.Options) == 0 {
fields.Options = strings.Join(optionText(cmd), "\n") fields.Options = strings.Join(optionText(width, cmd), "\n")
} }
if len(fields.Subcommands) == 0 { if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n") fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
} }
if len(fields.Synopsis) == 0 { if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr) fields.Synopsis = generateSynopsis(width, cmd, pathStr)
} }
// trim the extra newlines (see TrimNewlines doc) // trim the extra newlines (see TrimNewlines doc)
...@@ -212,21 +232,26 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer ...@@ -212,21 +232,26 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
fields := helpFields{ fields := helpFields{
Indent: indentStr, Indent: indentStr,
Path: pathStr, Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline, Tagline: cmd.Helptext.Tagline,
Synopsis: cmd.Helptext.Synopsis, Synopsis: cmd.Helptext.Synopsis,
Description: cmd.Helptext.ShortDescription, Description: cmd.Helptext.ShortDescription,
Subcommands: cmd.Helptext.Subcommands, Subcommands: cmd.Helptext.Subcommands,
Usage: cmd.Helptext.Usage,
MoreHelp: (cmd != root), MoreHelp: (cmd != root),
} }
width := getTerminalWidth(out) - len(indentStr)
// autogen fields that are empty // autogen fields that are empty
if len(cmd.Helptext.Usage) > 0 {
fields.Usage = cmd.Helptext.Usage
} else {
fields.Usage = commandUsageText(width, cmd, rootName, path)
}
if len(fields.Subcommands) == 0 { if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n") fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
} }
if len(fields.Synopsis) == 0 { if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr) fields.Synopsis = generateSynopsis(width, cmd, pathStr)
} }
// trim the extra newlines (see TrimNewlines doc) // trim the extra newlines (see TrimNewlines doc)
...@@ -238,8 +263,17 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer ...@@ -238,8 +263,17 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
return shortHelpTemplate.Execute(out, fields) return shortHelpTemplate.Execute(out, fields)
} }
func generateSynopsis(cmd *cmds.Command, path string) string { func generateSynopsis(width int, cmd *cmds.Command, path string) string {
res := path res := path
currentLineLength := len(res)
appendText := func(text string) {
if currentLineLength+len(text)+1 > width {
res += "\n" + strings.Repeat(" ", len(path))
currentLineLength = len(path)
}
currentLineLength += len(text) + 1
res += " " + text
}
for _, opt := range cmd.Options { for _, opt := range cmd.Options {
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Name()] valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Name()]
if !ok { if !ok {
...@@ -267,10 +301,10 @@ func generateSynopsis(cmd *cmds.Command, path string) string { ...@@ -267,10 +301,10 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
} }
} }
} }
res = fmt.Sprintf("%s [%s]", res, sopt) appendText("[" + sopt + "]")
} }
if len(cmd.Arguments) > 0 { if len(cmd.Arguments) > 0 {
res = fmt.Sprintf("%s [--]", res) appendText("[--]")
} }
for _, arg := range cmd.Arguments { for _, arg := range cmd.Arguments {
sarg := fmt.Sprintf("<%s>", arg.Name) sarg := fmt.Sprintf("<%s>", arg.Name)
...@@ -281,12 +315,12 @@ func generateSynopsis(cmd *cmds.Command, path string) string { ...@@ -281,12 +315,12 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
if !arg.Required { if !arg.Required {
sarg = fmt.Sprintf("[%s]", sarg) sarg = fmt.Sprintf("[%s]", sarg)
} }
res = fmt.Sprintf("%s %s", res, sarg) appendText(sarg)
} }
return strings.Trim(res, " ") return strings.Trim(res, " ")
} }
func argumentText(cmd *cmds.Command) []string { func argumentText(width int, cmd *cmds.Command) []string {
lines := make([]string, len(cmd.Arguments)) lines := make([]string, len(cmd.Arguments))
for i, arg := range cmd.Arguments { for i, arg := range cmd.Arguments {
...@@ -294,12 +328,39 @@ func argumentText(cmd *cmds.Command) []string { ...@@ -294,12 +328,39 @@ func argumentText(cmd *cmds.Command) []string {
} }
lines = align(lines) lines = align(lines)
for i, arg := range cmd.Arguments { for i, arg := range cmd.Arguments {
lines[i] += " - " + arg.Description lines[i] += " - "
lines[i] = appendWrapped(lines[i], arg.Description, width)
} }
return lines return lines
} }
func appendWrapped(prefix, text string, width int) string {
offset := len(prefix)
bWidth := width - offset
text = strings.Trim(text, whitespace)
// Minimum help-text width is 30 characters.
if bWidth < 30 {
prefix += text
return prefix
}
for len(text) > bWidth {
idx := strings.LastIndexAny(text[:bWidth], whitespace)
if idx < 0 {
idx = strings.IndexAny(text, whitespace)
}
if idx < 0 {
break
}
prefix += text[:idx] + "\n" + strings.Repeat(" ", offset)
text = strings.TrimLeft(text[idx:], whitespace)
}
prefix += text
return prefix
}
func optionFlag(flag string) string { func optionFlag(flag string) string {
if len(flag) == 1 { if len(flag) == 1 {
return fmt.Sprintf(shortFlag, flag) return fmt.Sprintf(shortFlag, flag)
...@@ -308,7 +369,7 @@ func optionFlag(flag string) string { ...@@ -308,7 +369,7 @@ func optionFlag(flag string) string {
} }
} }
func optionText(cmd ...*cmds.Command) []string { func optionText(width int, cmd ...*cmds.Command) []string {
// get a slice of the options we want to list out // get a slice of the options we want to list out
options := make([]cmds.Option, 0) options := make([]cmds.Option, 0)
for _, c := range cmd { for _, c := range cmd {
...@@ -317,53 +378,33 @@ func optionText(cmd ...*cmds.Command) []string { ...@@ -317,53 +378,33 @@ func optionText(cmd ...*cmds.Command) []string {
} }
} }
// add option names to output (with each name aligned) // add option names to output
lines := make([]string, 0) lines := make([]string, len(options))
j := 0 for i, opt := range options {
for { flags := sortByLength(opt.Names())
done := true for j, f := range flags {
i := 0 flags[j] = optionFlag(f)
for _, opt := range options {
if len(lines) < i+1 {
lines = append(lines, "")
}
names := sortByLength(opt.Names())
if len(names) >= j+1 {
lines[i] += optionFlag(names[j])
}
if len(names) > j+1 {
lines[i] += ", "
done = false
}
i++
}
if done {
break
} }
lines[i] = strings.Join(flags, ", ")
lines = align(lines)
j++
} }
lines = align(lines) lines = align(lines)
// add option types to output // add option types to output
for i, opt := range options { for i, opt := range options {
lines[i] += " " + fmt.Sprintf("%v", opt.Type()) lines[i] += " " + fmt.Sprintf("%v", opt.Type())
} }
lines = align(lines) lines = align(lines)
// add option descriptions to output // add option descriptions to output
for i, opt := range options { for i, opt := range options {
lines[i] += " - " + opt.Description() lines[i] += " - "
lines[i] = appendWrapped(lines[i], opt.Description(), width)
} }
return lines return lines
} }
func subcommandText(cmd *cmds.Command, rootName string, path []string) []string { func subcommandText(width int, cmd *cmds.Command, rootName string, path []string) []string {
prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " ")) prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
if len(path) > 0 { if len(path) > 0 {
prefix += " " prefix += " "
...@@ -392,12 +433,24 @@ func subcommandText(cmd *cmds.Command, rootName string, path []string) []string ...@@ -392,12 +433,24 @@ func subcommandText(cmd *cmds.Command, rootName string, path []string) []string
lines = align(lines) lines = align(lines)
for i, sub := range subcmds { for i, sub := range subcmds {
lines[i] += " - " + sub.Helptext.Tagline lines[i] += " - "
lines[i] = appendWrapped(lines[i], sub.Helptext.Tagline, width)
} }
return lines return lines
} }
func commandUsageText(width int, cmd *cmds.Command, rootName string, path []string) string {
text := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
argUsage := usageText(cmd)
if len(argUsage) > 0 {
text += " " + argUsage
}
text += " - "
text = appendWrapped(text, cmd.Helptext.Tagline, width)
return text
}
func usageText(cmd *cmds.Command) string { func usageText(cmd *cmds.Command) string {
s := "" s := ""
for i, arg := range cmd.Arguments { for i, arg := range cmd.Arguments {
......
...@@ -4,7 +4,7 @@ import ( ...@@ -4,7 +4,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ipfs/go-ipfs-cmds" cmds "github.com/ipfs/go-ipfs-cmds"
) )
func TestSynopsisGenerator(t *testing.T) { func TestSynopsisGenerator(t *testing.T) {
...@@ -22,7 +22,8 @@ func TestSynopsisGenerator(t *testing.T) { ...@@ -22,7 +22,8 @@ func TestSynopsisGenerator(t *testing.T) {
}, },
}, },
} }
syn := generateSynopsis(command, "cmd") terminalWidth := 100
syn := generateSynopsis(terminalWidth, command, "cmd")
t.Logf("Synopsis is: %s", syn) t.Logf("Synopsis is: %s", syn)
if !strings.HasPrefix(syn, "cmd ") { if !strings.HasPrefix(syn, "cmd ") {
t.Fatal("Synopsis should start with command name") t.Fatal("Synopsis should start with command name")
......
...@@ -6,4 +6,5 @@ require ( ...@@ -6,4 +6,5 @@ require (
github.com/ipfs/go-log v0.0.1 github.com/ipfs/go-log v0.0.1
github.com/rs/cors v1.6.0 github.com/rs/cors v1.6.0
github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
) )
...@@ -27,9 +27,18 @@ github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e h1: ...@@ -27,9 +27,18 @@ github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e h1:
github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e/go.mod h1:XDKHRm5ThF8YJjx001LtgelzsoaEcvnA7lVWz9EeX3g= github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e/go.mod h1:XDKHRm5ThF8YJjx001LtgelzsoaEcvnA7lVWz9EeX3g=
github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc h1:9lDbC6Rz4bwmou+oE6Dt4Cb2BGMur5eR/GYptkKUVHo= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc h1:9lDbC6Rz4bwmou+oE6Dt4Cb2BGMur5eR/GYptkKUVHo=
github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190227160552-c95aed5357e7 h1:C2F/nMkR/9sfUTpvR3QrjBuTdvMUC/cFajkphs1YLQo= golang.org/x/net v0.0.0-20190227160552-c95aed5357e7 h1:C2F/nMkR/9sfUTpvR3QrjBuTdvMUC/cFajkphs1YLQo=
golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190302025703-b6889370fb10 h1:xQJI9OEiErEQ++DoXOHqEpzsGMrAv2Q2jyCpi7DmfpQ= golang.org/x/sys v0.0.0-20190302025703-b6889370fb10 h1:xQJI9OEiErEQ++DoXOHqEpzsGMrAv2Q2jyCpi7DmfpQ=
golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment