blob: 20955fb5f92207c814a100e6dd97d2289443315f [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 (
"flag"
"fmt"
"io"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"v.io/x/lib/textutil"
)
// helpRunner is a Runner that implements the "help" functionality. Help is
// requested for the last command in rootPath, which must not be empty.
type helpRunner struct {
rootPath []*Command
*helpConfig
}
func makeHelpRunner(path []*Command, env *Env) helpRunner {
return helpRunner{path, &helpConfig{env.style(), env.width()}}
}
// 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
}
// Run implements the Runner interface method.
func (h helpRunner) Run(env *Env, args []string) error {
w := textutil.NewUTF8LineWriter(env.Stdout, h.width)
err := runHelp(w, env.Stderr, args, h.rootPath, h.helpConfig)
w.Flush()
return err
}
// usageFunc is used as the implementation of the Env.Usage function.
func (h helpRunner) usageFunc(writer io.Writer) {
w := textutil.NewUTF8LineWriter(writer, h.width)
usage(w, h.rootPath, h.helpConfig, true)
w.Flush()
}
const helpName = "help"
// newCommand returns a new help command that uses h as its Runner.
func (h helpRunner) newCommand() *Command {
help := &Command{
Runner: h,
Name: helpName,
Short: "Display help for commands or topics",
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.
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.LineWriter, stderr io.Writer, args []string, path []*Command, config *helpConfig) error {
if len(args) == 0 {
usage(w, path, config, true)
return nil
}
if args[0] == "..." {
usageAll(w, path, config, true)
return nil
}
// Look for matching children.
cmd, subName, subArgs := path[len(path)-1], args[0], args[1:]
for _, child := range cmd.Children {
if child.Name == subName {
return runHelp(w, stderr, subArgs, append(path, child), config)
}
}
if helpName == subName {
help := helpRunner{path, config}.newCommand()
return runHelp(w, stderr, subArgs, append(path, help), config)
}
// 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(stderr, fn, "%s: unknown command or topic %q", pathName(path), subName)
}
func godocHeader(s 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.
if s == "" {
return ""
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToUpper(r)) + s[n:]
}
func lineBreak(w *textutil.LineWriter, 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.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
cmd, cmdPath := path[len(path)-1], pathName(path)
if !firstCall {
lineBreak(w, config.style)
fmt.Fprintln(w, godocHeader(cmdPath))
fmt.Fprintln(w)
}
usage(w, path, config, firstCall)
for _, child := range cmd.Children {
usageAll(w, append(path, child), config, false)
}
if firstCall && needsHelpChild(cmd) {
help := helpRunner{path, config}.newCommand()
usageAll(w, append(path, help), config, false)
}
for _, topic := range cmd.Topics {
lineBreak(w, config.style)
fmt.Fprintln(w, godocHeader(cmdPath+" "+topic.Name+" - help topic"))
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.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
cmd, cmdPath := path[len(path)-1], pathName(path)
children := cmd.Children
if firstCall && needsHelpChild(cmd) {
help := helpRunner{path, config}.newCommand()
children = append(children, help)
}
fmt.Fprintln(w, cmd.Long)
fmt.Fprintln(w)
// Usage line.
fmt.Fprintln(w, "Usage:")
cmdPathF := " " + cmdPath
if countFlags(&cmd.Flags, nil, true) > 0 {
cmdPathF += " [flags]"
}
if cmd.Runner != nil {
if cmd.ArgsName != "" {
fmt.Fprintln(w, cmdPathF, cmd.ArgsName)
} else {
fmt.Fprintln(w, cmdPathF)
}
}
if len(children) > 0 {
fmt.Fprintln(w, cmdPathF, "<command>")
}
// Commands.
const minNameWidth = 11
if len(children) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The", cmdPath, "commands are:")
nameWidth := minNameWidth
for _, child := range children {
if len(child.Name) > nameWidth {
nameWidth = len(child.Name)
}
}
// Print as a table with aligned columns Name and Short.
w.SetIndents(spaces(3), spaces(3+nameWidth+1))
for _, child := range children {
fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, child.Name, child.Short)
w.Flush()
}
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 len(topic.Name) > nameWidth {
nameWidth = len(topic.Name)
}
}
// Print as a table with aligned columns Name and Short.
w.SetIndents(spaces(3), spaces(3+nameWidth+1))
for _, topic := range cmd.Topics {
fmt.Fprintf(w, "%-[1]*[2]s %[3]s", nameWidth, topic.Name, topic.Short)
w.Flush()
}
w.SetIndents()
if firstCall && config.style != styleGoDoc {
fmt.Fprintf(w, "Run \"%s help [topic]\" for topic details.\n", cmdPath)
}
}
flagsUsage(w, path, config, firstCall)
}
func flagsUsage(w *textutil.LineWriter, path []*Command, config *helpConfig, firstCall bool) {
cmd, cmdPath := path[len(path)-1], pathName(path)
// Flags.
if countFlags(&cmd.Flags, nil, true) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "The", cmdPath, "flags are:")
printFlags(w, &cmd.Flags, config.style, nil, true)
}
// Only show global flags on the first call.
if !firstCall {
return
}
hasCompact := countFlags(globalFlags, nonHiddenGlobalFlags, true) > 0
hasFull := countFlags(globalFlags, nonHiddenGlobalFlags, false) > 0
if config.style != styleCompact {
// Non-compact style, always show all global flags.
if hasCompact || hasFull {
fmt.Fprintln(w)
fmt.Fprintln(w, "The global flags are:")
printFlags(w, globalFlags, config.style, nonHiddenGlobalFlags, true)
if hasCompact && hasFull {
fmt.Fprintln(w)
}
printFlags(w, globalFlags, config.style, nonHiddenGlobalFlags, false)
}
return
}
// Compact style, only show compact flags and a reminder if there are more.
if hasCompact {
fmt.Fprintln(w)
fmt.Fprintln(w, "The global flags are:")
printFlags(w, globalFlags, config.style, nonHiddenGlobalFlags, true)
}
if hasFull {
fmt.Fprintln(w)
fullhelp := fmt.Sprintf(`Run "%s help -style=full" to show all global flags.`, cmdPath)
if len(cmd.Children) == 0 {
if len(path) > 1 {
parentPath := pathName(path[:len(path)-1])
fullhelp = fmt.Sprintf(`Run "%s help -style=full %s" to show all global flags.`, parentPath, cmd.Name)
} else {
fullhelp = fmt.Sprintf(`Run "CMDLINE_STYLE=full %s -help" to show all global flags.`, cmdPath)
}
}
fmt.Fprintln(w, fullhelp)
}
}
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.LineWriter, flags *flag.FlagSet, style style, regexps []*regexp.Regexp, match bool) {
flags.VisitAll(func(f *flag.Flag) {
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 nonHiddenGlobalFlags []*regexp.Regexp
// HideGlobalFlagsExcept hides global flags from the default compact-style usage
// message, except for the given regexps. Global flag names that match any of
// the regexps will still 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 HideGlobalFlagsExcept(regexps ...*regexp.Regexp) {
// NOTE: nonHiddenGlobalFlags is used as the argument to matchRegexps, where
// nil means "all names match" and empty means "no names match".
nonHiddenGlobalFlags = append(nonHiddenGlobalFlags, regexps...)
if nonHiddenGlobalFlags == nil {
nonHiddenGlobalFlags = []*regexp.Regexp{}
}
}