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

Hector Sanjuan's avatar
Hector Sanjuan committed
140 141
// ErrNoHelpRequested returns when request for help help does not include the
// short nor the long option.
142 143
var ErrNoHelpRequested = errors.New("no help requested")

Hector Sanjuan's avatar
Hector Sanjuan committed
144
// HandleHelp writes help to a writer for the given request's command.
145 146 147 148 149 150 151 152 153 154 155 156 157 158
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
	}
}

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

166 167 168
	pathStr := rootName
	if len(path) > 0 {
		pathStr += " " + strings.Join(path, " ")
169 170
	}

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

184
	width := getTerminalWidth(out) - len(indentStr)
185

186 187
	if len(cmd.Helptext.LongDescription) > 0 {
		fields.Description = cmd.Helptext.LongDescription
188 189
	}

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

209 210 211 212 213
	// trim the extra newlines (see TrimNewlines doc)
	fields.TrimNewlines()

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

215
	return longHelpTemplate.Execute(out, fields)
216 217
}

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

225 226 227 228 229
	// default cmd to root if there is no path
	if path == nil && cmd == nil {
		cmd = root
	}

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

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

245
	width := getTerminalWidth(out) - len(indentStr)
246

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

260 261 262 263 264
	// trim the extra newlines (see TrimNewlines doc)
	fields.TrimNewlines()

	// indent all fields that have been set
	fields.IndentAll()
265 266 267 268

	return shortHelpTemplate.Execute(out, fields)
}

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

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

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

331
func argumentText(width int, cmd *cmds.Command) []string {
332 333 334
	lines := make([]string, len(cmd.Arguments))

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

	return lines
}

346 347 348 349 350
func appendWrapped(prefix, text string, width int) string {
	offset := len(prefix)
	bWidth := width - offset

	text = strings.Trim(text, whitespace)
351 352
	// Minimum help-text width is 30 characters.
	if bWidth < 30 {
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
		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
372 373 374 375
func optionFlag(flag string) string {
	if len(flag) == 1 {
		return fmt.Sprintf(shortFlag, flag)
	}
Hector Sanjuan's avatar
Hector Sanjuan committed
376
	return fmt.Sprintf(longFlag, flag)
Etienne Laurin's avatar
Etienne Laurin committed
377 378
}

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

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
	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 indentString(line string, prefix string) string {
507
	return prefix + strings.Replace(line, "\n", "\n"+prefix, -1)
508
}
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529

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)
}