diff --git a/cli/helptext.go b/cli/helptext.go index 31e0da386c98b7e1d7828cd94251364e0f8ea5c8..1c5480a35433d46d918bc6046a9595160c54608e 100644 --- a/cli/helptext.go +++ b/cli/helptext.go @@ -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}} --help' for more information about each command. +{{.Indent}}For more information about each command, use: +{{.Indent}}'{{.Path}} --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}} --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 { diff --git a/cli/helptext_test.go b/cli/helptext_test.go index a516c287b9c16069c17fb6dfe5baa32669eb4f5a..c5f524c81b4f40241b2ab38a5fba47b8839ba0dc 100644 --- a/cli/helptext_test.go +++ b/cli/helptext_test.go @@ -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") diff --git a/go.mod b/go.mod index 546c3675e288eca8635dae02e4f112815fb11e79..abfc9321a5a9cca174af93486927c8fc3980b017 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 8ae2477cb4728bd37e9fd32bd8b0dda6b5597590..346423efb76607603f9cf8335ffc99654ec5faf0 100644 --- a/go.sum +++ b/go.sum @@ -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=