blob: e426f58df5266fff00da7d2f185b3c4b62137b6c [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmdline
import (
"bytes"
"flag"
"fmt"
"go/doc"
"io"
"path/filepath"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"go.fuchsia.dev/jiri/textutil"
)
const missingDescription = "No description available"
// helpRunner is a Runner that implements the "help" functionality. Help is
// requested for the last command in path, which must not be empty.
type helpRunner struct {
path []*Command
*helpConfig
}
func makeHelpRunner(path []*Command, env *Env) helpRunner {
return helpRunner{path, &helpConfig{
style: env.style(),
width: env.width(),
prefix: env.prefix(),
firstCall: env.firstCall(),
}}
}
// helpConfig holds configuration data for help. The style and width may be
// overriden by flags if the command returned by newCommand is parsed.
type helpConfig struct {
style style
width int
prefix string
firstCall bool
}
// Run implements the Runner interface method.
func (h helpRunner) Run(env *Env, args []string) error {
w := textutil.NewUTF8WrapWriter(env.Stdout, h.width)
defer w.Flush()
return runHelp(w, env, args, h.path, h.helpConfig)
}
// usageFunc is used as the implementation of the Env.Usage function.
func (h helpRunner) usageFunc(env *Env, writer io.Writer) {
w := textutil.NewUTF8WrapWriter(writer, h.width)
usage(w, env, h.path, h.helpConfig, h.helpConfig.firstCall)
w.Flush()
}
const (
helpName = "help"
helpShort = "Display help for commands or topics"
)
// newCommand returns a new help command that uses h as its Runner.
func (h helpRunner) newCommand() *Command {
help := &Command{
Runner: h,
Name: helpName,
Short: helpShort,
Long: `
Help with no args displays the usage of the parent command.
Help with args displays the usage of the specified sub-command or help topic.
"help ..." recursively displays help for all commands and topics.
`,
ArgsName: "[command/topic ...]",
ArgsLong: `
[command/topic ...] optionally identifies a specific sub-command or help topic.
`,
}
help.Flags.Var(&h.style, "style", `
The formatting style for help output:
compact - Good for compact cmdline output.
full - Good for cmdline output, shows all global flags.
godoc - Good for godoc processing.
shortonly - Only output short description.
Override the default by setting the CMDLINE_STYLE environment variable.
`)
help.Flags.IntVar(&h.width, "width", h.width, `
Format output to this target width in runes, or unlimited if width < 0.
Defaults to the terminal width if available. Override the default by setting
the CMDLINE_WIDTH environment variable.
`)
// Override default values, so that the godoc style shows good defaults.
help.Flags.Lookup("style").DefValue = "compact"
help.Flags.Lookup("width").DefValue = "<terminal width>"
cleanTree(help)
return help
}
// runHelp implements the run-time behavior of the help command.
func runHelp(w *textutil.WrapWriter, env *Env, args []string, path []*Command, config *helpConfig) error {
if len(args) == 0 {
usage(w, env, path, config, config.firstCall)
return nil
}
if args[0] == "..." {
usageAll(w, env, path, config, config.firstCall)
return nil
}
// Look for matching children.
cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
subName, subArgs := args[0], args[1:]
for _, child := range cmd.Children {
if child.Name == subName {
return runHelp(w, env, subArgs, append(path, child), config)
}
}
if helpName == subName {
help := helpRunner{path, config}.newCommand()
return runHelp(w, env, subArgs, append(path, help), config)
}
if cmd.LookPath {
// Look for a matching executable in PATH.
if subCmd, _ := env.LookPath(cmd.Name + "-" + subName); subCmd != "" {
runner := binaryRunner{subCmd, cmdPath}
envCopy := env.clone()
envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
if len(subArgs) == 0 {
return runner.Run(envCopy, []string{"-help"})
}
return runner.Run(envCopy, append([]string{helpName}, subArgs...))
}
}
// Look for matching topic.
for _, topic := range cmd.Topics {
if topic.Name == subName {
fmt.Fprintln(w, topic.Long)
return nil
}
}
fn := helpRunner{path, config}.usageFunc
return usageErrorf(env, fn, "%s: unknown command or topic %q", cmdPath, subName)
}
func godocHeader(path, short string) string {
// The first rune must be uppercase for godoc to recognize the string as a
// section header, which is linked to the table of contents.
switch {
case path == "":
return firstRuneToUpper(short)
case short == "":
return firstRuneToUpper(path)
}
// Godoc has special heurisitics to extract headers from the comments, from
// which it builds a nice table of contents. Headers must be single
// unindented lines with unindented paragraphs both before and after, and the
// line must not include certain characters.
//
// We try our best to create a header that includes both the command path and
// the short description, but if godoc won't extract a header out of the line,
// we fall back to just returning the command path.
//
// For more details see the comments and implementation of doc.ToHTML:
// http://golang.org/pkg/go/doc/#ToHTML
header := firstRuneToUpper(path + " - " + short)
var buf bytes.Buffer
doc.ToHTML(&buf, "before\n\n"+header+"\n\nafter", nil)
if !bytes.Contains(buf.Bytes(), []byte("<h")) {
return firstRuneToUpper(path)
}
return header
}
func firstRuneToUpper(s string) string {
if s == "" {
return ""
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToUpper(r)) + s[n:]
}
func lineBreak(w *textutil.WrapWriter, style style) {
w.Flush()
switch style {
case styleCompact, styleFull:
width := w.Width()
if width < 0 {
// If the user has chosen an "unlimited" word-wrapping width, we still
// need a reasonable width for our visual line break.
width = defaultWidth
}
fmt.Fprintln(w, strings.Repeat("=", width))
case styleGoDoc:
fmt.Fprintln(w)
}
w.Flush()
}
// needsHelpChild returns true if cmd needs a default help command to be
// appended to its children. Every command that has children and doesn't
// already have a "help" command needs a help child.
func needsHelpChild(cmd *Command) bool {
for _, child := range cmd.Children {
if child.Name == helpName {
return false
}
}
return len(cmd.Children) > 0
}
// usageAll prints usage recursively via DFS from the path onward.
func usageAll(w *textutil.WrapWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
usage(w, env, path, config, firstCall)
for _, child := range cmd.Children {
usageAll(w, env, append(path, child), config, false)
}
if firstCall && needsHelpChild(cmd) {
help := helpRunner{path, config}.newCommand()
usageAll(w, env, append(path, help), config, false)
}
if cmd.LookPath {
cmdPrefix := cmd.Name + "-"
subCmds, _ := env.LookPathPrefix(cmdPrefix, cmd.subNames(cmdPrefix))
for _, subCmd := range subCmds {
runner := binaryRunner{subCmd, cmdPath}
var buffer bytes.Buffer
envCopy := env.clone()
envCopy.Stdout = &buffer
envCopy.Stderr = &buffer
envCopy.Vars["CMDLINE_FIRST_CALL"] = "false"
envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
if err := runner.Run(envCopy, []string{helpName, "..."}); err == nil {
// The external child supports "help".
if config.style == styleGoDoc {
// The textutil package will discard any leading empty lines
// produced by the child process output, so we need to
// output it here.
fmt.Fprintln(w)
}
fmt.Fprint(w, buffer.String())
continue
}
buffer.Reset()
if err := runner.Run(envCopy, []string{"-help"}); err == nil {
// The external child supports "-help".
if config.style == styleGoDoc {
// The textutil package will discard any leading empty lines
// produced by the child process output, so we need to
// output it here.
fmt.Fprintln(w)
}
fmt.Fprint(w, buffer.String())
continue
}
// The external child does not support "help" or "-help".
lineBreak(w, config.style)
subName := strings.TrimPrefix(filepath.Base(subCmd), cmdPrefix)
fmt.Fprintln(w, godocHeader(cmdPath+" "+subName, missingDescription))
}
}
for _, topic := range cmd.Topics {
lineBreak(w, config.style)
w.ForceVerbatim(true)
fmt.Fprintln(w, godocHeader(cmdPath+" "+topic.Name, topic.Short))
w.ForceVerbatim(false)
fmt.Fprintln(w)
fmt.Fprintln(w, topic.Long)
}
}
// usage prints the usage of the last command in path to w. The bool firstCall
// is set to false when printing usage for multiple commands, and is used to
// avoid printing redundant information (e.g. help command, global flags).
func usage(w *textutil.WrapWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
env.TimerPush("usage " + cmdPath)
defer env.TimerPop()
if config.style == styleShortOnly {
fmt.Fprintln(w, cmd.Short)
return
}
if !firstCall {
lineBreak(w, config.style)
w.ForceVerbatim(true)
fmt.Fprintln(w, godocHeader(cmdPath, cmd.Short))
w.ForceVerbatim(false)
fmt.Fprintln(w)
}
fmt.Fprintln(w, cmd.Long)
fmt.Fprintln(w)
// Usage line.
fmt.Fprintln(w, "Usage:")
cmdPathF := " " + cmdPath
if countFlags(pathFlags(path), nil, true) > 0 || countFlags(globalFlags, nil, true) > 0 {
cmdPathF += " [flags]"
}
if cmd.Runner != nil {
if cmd.ArgsName != "" {
fmt.Fprintln(w, cmdPathF, cmd.ArgsName)
} else {
fmt.Fprintln(w, cmdPathF)
}
}
var extChildren []string
cmdPrefix := cmd.Name + "-"
if cmd.LookPath {
extChildren, _ = env.LookPathPrefix(cmdPrefix, cmd.subNames(cmdPrefix))
}
hasSubcommands := len(cmd.Children) > 0 || len(extChildren) > 0
if hasSubcommands {
fmt.Fprintln(w, cmdPathF, "<command>")
fmt.Fprintln(w)
}
printShort := func(width int, name, short string) {
fmt.Fprintf(w, "%-[1]*[2]s %[3]s", width, name, short)
w.Flush()
}
const minNameWidth = 11
nameWidth := minNameWidth
for _, child := range cmd.Children {
if w := len(child.Name); w > nameWidth {
nameWidth = w
}
}
for _, extCmd := range extChildren {
extName := strings.TrimPrefix(filepath.Base(extCmd), cmdPrefix)
if w := len(extName); w > nameWidth {
nameWidth = w
}
}
// Built-in commands.
if len(cmd.Children) > 0 {
w.SetIndents()
fmt.Fprintln(w, "The", cmdPath, "commands are:")
// Print as a table with aligned columns Name and Short.
w.SetIndents(spaces(3), spaces(3+nameWidth+1))
for _, child := range cmd.Children {
printShort(nameWidth, child.Name, child.Short)
}
// Default help command.
if firstCall && needsHelpChild(cmd) {
printShort(nameWidth, helpName, helpShort)
}
}
// External commands.
if len(extChildren) > 0 {
w.SetIndents()
fmt.Fprintln(w, "The", cmdPath, "external commands are:")
// Print as a table with aligned columns Name and Short.
w.SetIndents(spaces(3), spaces(3+nameWidth+1))
for _, extCmd := range extChildren {
runner := binaryRunner{extCmd, cmdPath}
var buffer bytes.Buffer
envCopy := env.clone()
envCopy.Stdout = &buffer
envCopy.Stderr = &buffer
envCopy.Vars["CMDLINE_STYLE"] = "shortonly"
short := missingDescription
if err := runner.Run(envCopy, []string{"-help"}); err == nil {
// The external child supports "-help".
short = buffer.String()
}
extName := strings.TrimPrefix(filepath.Base(extCmd), cmdPrefix)
printShort(nameWidth, extName, short)
}
}
// Command footer.
if hasSubcommands {
w.SetIndents()
if firstCall && config.style != styleGoDoc {
fmt.Fprintf(w, "Run \"%s help [command]\" for command usage.\n", cmdPath)
}
}
// Args.
if cmd.Runner != nil && cmd.ArgsLong != "" {
fmt.Fprintln(w)
fmt.Fprintln(w, cmd.ArgsLong)
}
// Help topics.
if len(cmd.Topics) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The", cmdPath, "additional help topics are:")
nameWidth := minNameWidth
for _, topic := range cmd.Topics {
if w := len(topic.Name); w > nameWidth {
nameWidth = w
}
}
// Print as a table with aligned columns Name and Short.
w.SetIndents(spaces(3), spaces(3+nameWidth+1))
for _, topic := range cmd.Topics {
printShort(nameWidth, topic.Name, topic.Short)
}
w.SetIndents()
if firstCall && config.style != styleGoDoc {
fmt.Fprintf(w, "Run \"%s help [topic]\" for topic details.\n", cmdPath)
}
}
hidden := flagsUsage(w, path, config)
// Only show global flags on the first call.
if firstCall {
hidden = globalFlagsUsage(w, config) || hidden
}
if hidden {
fmt.Fprintln(w)
fullhelp := fmt.Sprintf(`Run "%s help -style=full" to show all flags.`, cmdPath)
if len(cmd.Children) == 0 {
if len(path) > 1 {
parentPath := pathName(config.prefix, path[:len(path)-1])
fullhelp = fmt.Sprintf(`Run "%s help -style=full %s" to show all flags.`, parentPath, cmd.Name)
} else {
fullhelp = fmt.Sprintf(`Run "CMDLINE_STYLE=full %s -help" to show all flags.`, cmdPath)
}
}
fmt.Fprintln(w, fullhelp)
}
}
func flagsUsage(w *textutil.WrapWriter, path []*Command, config *helpConfig) bool {
cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
allFlags := pathFlags(path)
numCompact := countFlags(&cmd.Flags, nil, true)
numFull := countFlags(allFlags, nil, true) - numCompact
if config.style == styleCompact {
// Compact style, only show compact flags.
if numCompact > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The", cmdPath, "flags are:")
printFlags(w, &cmd.Flags, nil, config.style, nil, true)
}
return numFull > 0
}
// Non-compact style, always show all flags.
if numCompact > 0 || numFull > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The", cmdPath, "flags are:")
printFlags(w, &cmd.Flags, nil, config.style, nil, true)
if numCompact > 0 && numFull > 0 {
fmt.Fprintln(w)
}
printFlags(w, allFlags, &cmd.Flags, config.style, nil, true)
}
return false
}
func globalFlagsUsage(w *textutil.WrapWriter, config *helpConfig) bool {
regex := regexp.MustCompilePOSIX("^test\\..*$")
HideGlobalFlags(regex)
numCompact := countFlags(globalFlags, hiddenGlobalFlags, false)
numFull := countFlags(globalFlags, hiddenGlobalFlags, true)
if config.style == styleCompact {
// Compact style, only show compact flags.
if numCompact > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The global flags are:")
printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, false)
}
return numFull > 0
}
// Non-compact style, always show all global flags.
if numCompact > 0 || numFull > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The global flags are:")
printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, false)
if numCompact > 0 && numFull > 0 {
fmt.Fprintln(w)
}
printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, true)
}
return false
}
func countFlags(flags *flag.FlagSet, regexps []*regexp.Regexp, match bool) (num int) {
flags.VisitAll(func(f *flag.Flag) {
if match == matchRegexps(regexps, f.Name) {
num++
}
})
return
}
func printFlags(w *textutil.WrapWriter, flags, filter *flag.FlagSet, style style, regexps []*regexp.Regexp, match bool) {
flags.VisitAll(func(f *flag.Flag) {
if filter != nil && filter.Lookup(f.Name) != nil {
return
}
if match != matchRegexps(regexps, f.Name) {
return
}
value := f.Value.String()
if style == styleGoDoc {
// When using styleGoDoc we use the default value, so that e.g. regular
// help will show "/usr/home/me/foo" while godoc will show "$HOME/foo".
value = f.DefValue
}
fmt.Fprintf(w, " -%s=%v", f.Name, value)
w.SetIndents(spaces(3))
fmt.Fprintln(w, f.Usage)
w.SetIndents()
})
}
func spaces(count int) string {
return strings.Repeat(" ", count)
}
func matchRegexps(regexps []*regexp.Regexp, name string) bool {
// We distinguish nil regexps from empty regexps; the former means "all names
// match", while the latter means "no names match".
if regexps == nil {
return true
}
for _, r := range regexps {
if r.MatchString(name) {
return true
}
}
return false
}
var hiddenGlobalFlags []*regexp.Regexp
// HideGlobalFlags hides global flags from the default compact-style usage
// message. Global flag names that match any of the regexps will not be shown
// in the compact usage message. Multiple calls behave as if all regexps were
// provided in a single call.
//
// All global flags are always shown in non-compact style usage messages.
func HideGlobalFlags(regexps ...*regexp.Regexp) {
hiddenGlobalFlags = append(hiddenGlobalFlags, regexps...)
if hiddenGlobalFlags == nil {
hiddenGlobalFlags = []*regexp.Regexp{}
}
}