helptext.go 12 KB
Newer Older
1 2 3
package cli

import (
4
	"errors"
5
	"fmt"
6
	"io"
7
	"os"
8
	"sort"
9
	"strings"
10
	"text/template"
11

12 13
	cmds "github.com/ipfs/go-ipfs-cmds"
	"golang.org/x/crypto/ssh/terminal"
14 15 16
)

const (
17 18 19 20 21 22 23
	defaultTerminalWidth = 80
	requiredArg          = "<%v>"
	optionalArg          = "[<%v>]"
	variadicArg          = "%v..."
	shortFlag            = "-%v"
	longFlag             = "--%v"
	optionType           = "(%v)"
24 25

	whitespace = "\r\n\t "
26

27
	indentStr = "  "
28 29
)

30 31
type helpFields struct {
	Indent      string
32
	Usage       string
33 34 35 36
	Path        string
	Tagline     string
	Arguments   string
	Options     string
37
	Synopsis    string
38 39
	Subcommands string
	Description string
40
	MoreHelp    bool
41 42
}

43 44 45 46 47 48 49 50 51 52
// TrimNewlines removes extra newlines from fields. This makes aligning
// commands easier. Below, the leading + tralining newlines are removed:
//	Synopsis: `
//	    ipfs config <key>          - Get value of <key>
//	    ipfs config <key> <value>  - Set value of <key> to <value>
//	    ipfs config --show         - Show config file
//	    ipfs config --edit         - Edit config file in $EDITOR
//	`
func (f *helpFields) TrimNewlines() {
	f.Path = strings.Trim(f.Path, "\n")
Steven Allen's avatar
Steven Allen committed
53
	f.Usage = strings.Trim(f.Usage, "\n")
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
	f.Tagline = strings.Trim(f.Tagline, "\n")
	f.Arguments = strings.Trim(f.Arguments, "\n")
	f.Options = strings.Trim(f.Options, "\n")
	f.Synopsis = strings.Trim(f.Synopsis, "\n")
	f.Subcommands = strings.Trim(f.Subcommands, "\n")
	f.Description = strings.Trim(f.Description, "\n")
}

// Indent adds whitespace the lines of fields.
func (f *helpFields) IndentAll() {
	indent := func(s string) string {
		if s == "" {
			return s
		}
		return indentString(s, indentStr)
	}

Steven Allen's avatar
Steven Allen committed
71
	f.Usage = indent(f.Usage)
72 73 74 75 76 77 78
	f.Arguments = indent(f.Arguments)
	f.Options = indent(f.Options)
	f.Synopsis = indent(f.Synopsis)
	f.Subcommands = indent(f.Subcommands)
	f.Description = indent(f.Description)
}

79
const longHelpFormat = `USAGE
Steven Allen's avatar
Steven Allen committed
80
{{.Usage}}
81

Richard Littauer's avatar
Richard Littauer committed
82 83 84 85
{{if .Synopsis}}SYNOPSIS
{{.Synopsis}}

{{end}}{{if .Arguments}}ARGUMENTS
86

87
{{.Arguments}}
88

89
{{end}}{{if .Options}}OPTIONS
90

91
{{.Options}}
92

93
{{end}}{{if .Description}}DESCRIPTION
94

95 96 97
{{.Description}}

{{end}}{{if .Subcommands}}SUBCOMMANDS
98
{{.Subcommands}}
99

100 101
{{.Indent}}For more information about each command, use:
{{.Indent}}'{{.Path}} <subcmd> --help'
102 103
{{end}}
`
104
const shortHelpFormat = `USAGE
Steven Allen's avatar
Steven Allen committed
105
{{.Usage}}
106 107 108 109
{{if .Synopsis}}
{{.Synopsis}}
{{end}}{{if .Description}}
{{.Description}}
110 111 112 113
{{end}}{{if .Subcommands}}
SUBCOMMANDS
{{.Subcommands}}
{{end}}{{if .MoreHelp}}
114 115
{{.Indent}}For more information about each command, use:
{{.Indent}}'{{.Path}} <subcmd> --help'
116
{{end}}
117
`
118

119 120
var longHelpTemplate *template.Template
var shortHelpTemplate *template.Template
121

122 123 124 125 126 127 128 129 130 131 132 133 134
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
}

135
func init() {
Steven Allen's avatar
Steven Allen committed
136 137
	longHelpTemplate = template.Must(template.New("longHelp").Parse(longHelpFormat))
	shortHelpTemplate = template.Must(template.New("shortHelp").Parse(shortHelpFormat))
138
}
139

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
var ErrNoHelpRequested = errors.New("no help requested")

func HandleHelp(appName string, req *cmds.Request, out io.Writer) error {
	long, _ := req.Options[cmds.OptLongHelp].(bool)
	short, _ := req.Options[cmds.OptShortHelp].(bool)

	switch {
	case long:
		return LongHelp(appName, req.Root, req.Path, out)
	case short:
		return ShortHelp(appName, req.Root, req.Path, out)
	default:
		return ErrNoHelpRequested
	}
}

156
// LongHelp writes a formatted CLI helptext string to a Writer for the given command
157 158 159 160
func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
	cmd, err := root.Get(path)
	if err != nil {
		return err
161 162
	}

163 164 165
	pathStr := rootName
	if len(path) > 0 {
		pathStr += " " + strings.Join(path, " ")
166 167
	}

168 169 170
	fields := helpFields{
		Indent:      indentStr,
		Path:        pathStr,
171 172 173
		Tagline:     cmd.Helptext.Tagline,
		Arguments:   cmd.Helptext.Arguments,
		Options:     cmd.Helptext.Options,
174
		Synopsis:    cmd.Helptext.Synopsis,
175 176 177
		Subcommands: cmd.Helptext.Subcommands,
		Description: cmd.Helptext.ShortDescription,
		Usage:       cmd.Helptext.Usage,
178
		MoreHelp:    (cmd != root),
179 180
	}

181
	width := getTerminalWidth(out) - len(indentStr)
182

183 184
	if len(cmd.Helptext.LongDescription) > 0 {
		fields.Description = cmd.Helptext.LongDescription
185 186
	}

187
	// autogen fields that are empty
Steven Allen's avatar
Steven Allen committed
188 189 190 191 192
	if len(cmd.Helptext.Usage) > 0 {
		fields.Usage = cmd.Helptext.Usage
	} else {
		fields.Usage = commandUsageText(width, cmd, rootName, path)
	}
193
	if len(fields.Arguments) == 0 {
194
		fields.Arguments = strings.Join(argumentText(width, cmd), "\n")
195
	}
196
	if len(fields.Options) == 0 {
197
		fields.Options = strings.Join(optionText(width, cmd), "\n")
198
	}
199
	if len(fields.Subcommands) == 0 {
Steven Allen's avatar
Steven Allen committed
200
		fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
201
	}
Jakub Sztandera's avatar
Jakub Sztandera committed
202
	if len(fields.Synopsis) == 0 {
203
		fields.Synopsis = generateSynopsis(width, cmd, pathStr)
Jakub Sztandera's avatar
Jakub Sztandera committed
204
	}
205

206 207 208 209 210
	// trim the extra newlines (see TrimNewlines doc)
	fields.TrimNewlines()

	// indent all fields that have been set
	fields.IndentAll()
211

212
	return longHelpTemplate.Execute(out, fields)
213 214
}

215
// ShortHelp writes a formatted CLI helptext string to a Writer for the given command
216 217 218 219 220 221
func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
	cmd, err := root.Get(path)
	if err != nil {
		return err
	}

222 223 224 225 226
	// default cmd to root if there is no path
	if path == nil && cmd == nil {
		cmd = root
	}

227 228 229 230 231 232 233 234
	pathStr := rootName
	if len(path) > 0 {
		pathStr += " " + strings.Join(path, " ")
	}

	fields := helpFields{
		Indent:      indentStr,
		Path:        pathStr,
235
		Tagline:     cmd.Helptext.Tagline,
236
		Synopsis:    cmd.Helptext.Synopsis,
237
		Description: cmd.Helptext.ShortDescription,
238
		Subcommands: cmd.Helptext.Subcommands,
239
		MoreHelp:    (cmd != root),
240 241
	}

242
	width := getTerminalWidth(out) - len(indentStr)
243

244
	// autogen fields that are empty
Steven Allen's avatar
Steven Allen committed
245 246 247 248 249
	if len(cmd.Helptext.Usage) > 0 {
		fields.Usage = cmd.Helptext.Usage
	} else {
		fields.Usage = commandUsageText(width, cmd, rootName, path)
	}
250
	if len(fields.Subcommands) == 0 {
Steven Allen's avatar
Steven Allen committed
251
		fields.Subcommands = strings.Join(subcommandText(width, cmd, rootName, path), "\n")
252
	}
Jakub Sztandera's avatar
Jakub Sztandera committed
253
	if len(fields.Synopsis) == 0 {
254
		fields.Synopsis = generateSynopsis(width, cmd, pathStr)
Jakub Sztandera's avatar
Jakub Sztandera committed
255
	}
256

257 258 259 260 261
	// trim the extra newlines (see TrimNewlines doc)
	fields.TrimNewlines()

	// indent all fields that have been set
	fields.IndentAll()
262 263 264 265

	return shortHelpTemplate.Execute(out, fields)
}

266
func generateSynopsis(width int, cmd *cmds.Command, path string) string {
Jakub Sztandera's avatar
Jakub Sztandera committed
267
	res := path
268 269 270 271 272 273 274 275 276
	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
	}
Jakub Sztandera's avatar
Jakub Sztandera committed
277
	for _, opt := range cmd.Options {
278
		valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Name()]
Jakub Sztandera's avatar
Jakub Sztandera committed
279
		if !ok {
280
			valopt = opt.Name()
Jakub Sztandera's avatar
Jakub Sztandera committed
281 282 283 284 285 286 287
		}
		sopt := ""
		for i, n := range opt.Names() {
			pre := "-"
			if len(n) > 1 {
				pre = "--"
			}
Steven Allen's avatar
Steven Allen committed
288
			if opt.Type() == cmds.Bool && opt.Default() == true {
289 290 291
				pre = "--"
				sopt = fmt.Sprintf("%s%s=false", pre, n)
				break
Jakub Sztandera's avatar
Jakub Sztandera committed
292
			} else {
293
				if i == 0 {
Steven Allen's avatar
Steven Allen committed
294
					if opt.Type() == cmds.Bool {
295 296 297 298 299 300 301
						sopt = fmt.Sprintf("%s%s", pre, n)
					} else {
						sopt = fmt.Sprintf("%s%s=<%s>", pre, n, valopt)
					}
				} else {
					sopt = fmt.Sprintf("%s | %s%s", sopt, pre, n)
				}
Jakub Sztandera's avatar
Jakub Sztandera committed
302 303
			}
		}
304 305 306 307 308 309

		if opt.Type() == cmds.Strings {
			appendText("[" + sopt + "]...")
		} else {
			appendText("[" + sopt + "]")
		}
Jakub Sztandera's avatar
Jakub Sztandera committed
310 311
	}
	if len(cmd.Arguments) > 0 {
312
		appendText("[--]")
Jakub Sztandera's avatar
Jakub Sztandera committed
313 314 315 316 317 318 319 320 321 322
	}
	for _, arg := range cmd.Arguments {
		sarg := fmt.Sprintf("<%s>", arg.Name)
		if arg.Variadic {
			sarg = sarg + "..."
		}

		if !arg.Required {
			sarg = fmt.Sprintf("[%s]", sarg)
		}
323
		appendText(sarg)
Jakub Sztandera's avatar
Jakub Sztandera committed
324 325 326 327
	}
	return strings.Trim(res, " ")
}

328
func argumentText(width int, cmd *cmds.Command) []string {
329 330 331
	lines := make([]string, len(cmd.Arguments))

	for i, arg := range cmd.Arguments {
332 333 334 335
		lines[i] = argUsageText(arg)
	}
	lines = align(lines)
	for i, arg := range cmd.Arguments {
336 337
		lines[i] += " - "
		lines[i] = appendWrapped(lines[i], arg.Description, width)
338 339 340 341 342
	}

	return lines
}

343 344 345 346 347
func appendWrapped(prefix, text string, width int) string {
	offset := len(prefix)
	bWidth := width - offset

	text = strings.Trim(text, whitespace)
348 349
	// Minimum help-text width is 30 characters.
	if bWidth < 30 {
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
		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
}

Etienne Laurin's avatar
Etienne Laurin committed
369 370 371 372 373 374 375 376
func optionFlag(flag string) string {
	if len(flag) == 1 {
		return fmt.Sprintf(shortFlag, flag)
	} else {
		return fmt.Sprintf(longFlag, flag)
	}
}

377
func optionText(width int, cmd ...*cmds.Command) []string {
378
	// get a slice of the options we want to list out
Steven Allen's avatar
Steven Allen committed
379
	options := make([]cmds.Option, 0)
380 381 382 383 384 385
	for _, c := range cmd {
		for _, opt := range c.Options {
			options = append(options, opt)
		}
	}

Steven Allen's avatar
Steven Allen committed
386 387 388 389 390 391
	// 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)
392
		}
Steven Allen's avatar
Steven Allen committed
393
		lines[i] = strings.Join(flags, ", ")
394
	}
395
	lines = align(lines)
396 397 398

	// add option types to output
	for i, opt := range options {
399
		lines[i] += "  " + fmt.Sprintf("%v", opt.Type())
400 401 402 403 404
	}
	lines = align(lines)

	// add option descriptions to output
	for i, opt := range options {
405 406
		lines[i] += " - "
		lines[i] = appendWrapped(lines[i], opt.Description(), width)
407 408 409 410 411
	}

	return lines
}

Steven Allen's avatar
Steven Allen committed
412
func subcommandText(width int, cmd *cmds.Command, rootName string, path []string) []string {
413
	prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
414 415 416
	if len(path) > 0 {
		prefix += " "
	}
417 418 419 420 421 422 423 424

	// Sorting fixes changing order bug #2981.
	sortedNames := make([]string, 0)
	for name := range cmd.Subcommands {
		sortedNames = append(sortedNames, name)
	}
	sort.Strings(sortedNames)

425 426
	subcmds := make([]*cmds.Command, len(cmd.Subcommands))
	lines := make([]string, len(cmd.Subcommands))
427

428 429
	for i, name := range sortedNames {
		sub := cmd.Subcommands[name]
430
		usage := usageText(sub)
Matt Bell's avatar
Matt Bell committed
431 432 433
		if len(usage) > 0 {
			usage = " " + usage
		}
434

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
435 436
		lines[i] = prefix + name + usage
		subcmds[i] = sub
437 438
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
439 440
	lines = align(lines)
	for i, sub := range subcmds {
Steven Allen's avatar
Steven Allen committed
441 442
		lines[i] += " - "
		lines[i] = appendWrapped(lines[i], sub.Helptext.Tagline, width)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
443 444
	}

445 446 447
	return lines
}

Steven Allen's avatar
Steven Allen committed
448 449 450 451 452 453 454 455 456 457 458
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
}

459 460 461 462 463 464 465 466 467 468 469 470
func usageText(cmd *cmds.Command) string {
	s := ""
	for i, arg := range cmd.Arguments {
		if i != 0 {
			s += " "
		}
		s += argUsageText(arg)
	}

	return s
}

Steven Allen's avatar
Steven Allen committed
471
func argUsageText(arg cmds.Argument) string {
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
	s := arg.Name

	if arg.Required {
		s = fmt.Sprintf(requiredArg, s)
	} else {
		s = fmt.Sprintf(optionalArg, s)
	}

	if arg.Variadic {
		s = fmt.Sprintf(variadicArg, s)
	}

	return s
}

func align(lines []string) []string {
	longest := 0
	for _, line := range lines {
		length := len(line)
		if length > longest {
			longest = length
		}
	}

	for i, line := range lines {
		length := len(line)
		if length > 0 {
			lines[i] += strings.Repeat(" ", longest-length)
		}
	}

	return lines
}

func indent(lines []string, prefix string) []string {
	for i, line := range lines {
		lines[i] = prefix + indentString(line, prefix)
	}
	return lines
}

func indentString(line string, prefix string) string {
514
	return prefix + strings.Replace(line, "\n", "\n"+prefix, -1)
515
}
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536

type lengthSlice []string

func (ls lengthSlice) Len() int {
	return len(ls)
}
func (ls lengthSlice) Swap(a, b int) {
	ls[a], ls[b] = ls[b], ls[a]
}
func (ls lengthSlice) Less(a, b int) bool {
	return len(ls[a]) < len(ls[b])
}

func sortByLength(slice []string) []string {
	output := make(lengthSlice, len(slice))
	for i, val := range slice {
		output[i] = val
	}
	sort.Sort(output)
	return []string(output)
}