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 (
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
"text/template"
"github.com/ipfs/go-ipfs-cmds"
cmds "github.com/ipfs/go-ipfs-cmds"
"golang.org/x/crypto/ssh/terminal"
)
const (
requiredArg = "<%v>"
optionalArg = "[<%v>]"
variadicArg = "%v..."
shortFlag = "-%v"
longFlag = "--%v"
optionType = "(%v)"
defaultTerminalWidth = 80
requiredArg = "<%v>"
optionalArg = "[<%v>]"
variadicArg = "%v..."
shortFlag = "-%v"
longFlag = "--%v"
optionType = "(%v)"
whitespace = "\r\n\t "
......@@ -28,7 +31,6 @@ type helpFields struct {
Indent string
Usage string
Path string
ArgUsage string
Tagline string
Arguments string
Options string
......@@ -48,7 +50,7 @@ type helpFields struct {
// `
func (f *helpFields) TrimNewlines() {
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.Arguments = strings.Trim(f.Arguments, "\n")
f.Options = strings.Trim(f.Options, "\n")
......@@ -66,6 +68,7 @@ func (f *helpFields) IndentAll() {
return indentString(s, indentStr)
}
f.Usage = indent(f.Usage)
f.Arguments = indent(f.Arguments)
f.Options = indent(f.Options)
f.Synopsis = indent(f.Synopsis)
......@@ -73,10 +76,8 @@ func (f *helpFields) IndentAll() {
f.Description = indent(f.Description)
}
const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
const longHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}}
{{.Usage}}
{{if .Synopsis}}SYNOPSIS
{{.Synopsis}}
......@@ -96,11 +97,12 @@ const longHelpFormat = `USAGE
{{end}}{{if .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}}
`
const shortHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}}
{{.Usage}}
{{if .Synopsis}}
{{.Synopsis}}
{{end}}{{if .Description}}
......@@ -109,18 +111,30 @@ const shortHelpFormat = `USAGE
SUBCOMMANDS
{{.Subcommands}}
{{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}}
`
var usageTemplate *template.Template
var longHelpTemplate *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() {
usageTemplate = template.Must(template.New("usage").Parse(usageFormat))
longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat))
shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
longHelpTemplate = template.Must(template.New("longHelp").Parse(longHelpFormat))
shortHelpTemplate = template.Must(template.New("shortHelp").Parse(shortHelpFormat))
}
var ErrNoHelpRequested = errors.New("no help requested")
......@@ -154,7 +168,6 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
fields := helpFields{
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Arguments: cmd.Helptext.Arguments,
Options: cmd.Helptext.Options,
......@@ -165,22 +178,29 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
MoreHelp: (cmd != root),
}
width := getTerminalWidth(out) - len(indentStr)
if len(cmd.Helptext.LongDescription) > 0 {
fields.Description = cmd.Helptext.LongDescription
}
// 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 {
fields.Arguments = strings.Join(argumentText(cmd), "\n")
fields.Arguments = strings.Join(argumentText(width, cmd), "\n")
}
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 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
}
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
fields.Synopsis = generateSynopsis(width, cmd, pathStr)
}
// trim the extra newlines (see TrimNewlines doc)
......@@ -212,21 +232,26 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
fields := helpFields{
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Synopsis: cmd.Helptext.Synopsis,
Description: cmd.Helptext.ShortDescription,
Subcommands: cmd.Helptext.Subcommands,
Usage: cmd.Helptext.Usage,
MoreHelp: (cmd != root),
}
width := getTerminalWidth(out) - len(indentStr)
// 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 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
}
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
fields.Synopsis = generateSynopsis(width, cmd, pathStr)
}
// trim the extra newlines (see TrimNewlines doc)
......@@ -238,8 +263,17 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
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
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 {
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Name()]
if !ok {
......@@ -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 {
res = fmt.Sprintf("%s [--]", res)
appendText("[--]")
}
for _, arg := range cmd.Arguments {
sarg := fmt.Sprintf("<%s>", arg.Name)
......@@ -281,12 +315,12 @@ func generateSynopsis(cmd *cmds.Command, path string) string {
if !arg.Required {
sarg = fmt.Sprintf("[%s]", sarg)
}
res = fmt.Sprintf("%s %s", res, sarg)
appendText(sarg)
}
return strings.Trim(res, " ")
}
func argumentText(cmd *cmds.Command) []string {
func argumentText(width int, cmd *cmds.Command) []string {
lines := make([]string, len(cmd.Arguments))
for i, arg := range cmd.Arguments {
......@@ -294,12 +328,39 @@ func argumentText(cmd *cmds.Command) []string {
}
lines = align(lines)
for i, arg := range cmd.Arguments {
lines[i] += " - " + arg.Description
lines[i] += " - "
lines[i] = appendWrapped(lines[i], arg.Description, width)
}
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 {
if len(flag) == 1 {
return fmt.Sprintf(shortFlag, flag)
......@@ -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
options := make([]cmds.Option, 0)
for _, c := range cmd {
......@@ -317,53 +378,33 @@ func optionText(cmd ...*cmds.Command) []string {
}
}
// add option names to output (with each name aligned)
lines := make([]string, 0)
j := 0
for {
done := true
i := 0
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
// add option names to output
lines := make([]string, len(options))
for i, opt := range options {
flags := sortByLength(opt.Names())
for j, f := range flags {
flags[j] = optionFlag(f)
}
lines = align(lines)
j++
lines[i] = strings.Join(flags, ", ")
}
lines = align(lines)
// add option types to output
for i, opt := range options {
lines[i] += " " + fmt.Sprintf("%v", opt.Type())
lines[i] += " " + fmt.Sprintf("%v", opt.Type())
}
lines = align(lines)
// add option descriptions to output
for i, opt := range options {
lines[i] += " - " + opt.Description()
lines[i] += " - "
lines[i] = appendWrapped(lines[i], opt.Description(), width)
}
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, " "))
if len(path) > 0 {
prefix += " "
......@@ -392,12 +433,24 @@ func subcommandText(cmd *cmds.Command, rootName string, path []string) []string
lines = align(lines)
for i, sub := range subcmds {
lines[i] += " - " + sub.Helptext.Tagline
lines[i] += " - "
lines[i] = appendWrapped(lines[i], sub.Helptext.Tagline, width)
}
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 {
s := ""
for i, arg := range cmd.Arguments {
......
......@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"github.com/ipfs/go-ipfs-cmds"
cmds "github.com/ipfs/go-ipfs-cmds"
)
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)
if !strings.HasPrefix(syn, "cmd ") {
t.Fatal("Synopsis should start with command name")
......
......@@ -6,4 +6,5 @@ require (
github.com/ipfs/go-log v0.0.1
github.com/rs/cors v1.6.0
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:
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/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/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-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-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=
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