github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cmdline/help.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cmdline
     6  
     7  import (
     8  	"bytes"
     9  	"flag"
    10  	"fmt"
    11  	"go/doc"
    12  	"io"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  	"unicode"
    17  	"unicode/utf8"
    18  
    19  	"github.com/btwiuse/jiri/textutil"
    20  )
    21  
    22  const missingDescription = "No description available"
    23  
    24  // helpRunner is a Runner that implements the "help" functionality.  Help is
    25  // requested for the last command in path, which must not be empty.
    26  type helpRunner struct {
    27  	path []*Command
    28  	*helpConfig
    29  }
    30  
    31  func makeHelpRunner(path []*Command, env *Env) helpRunner {
    32  	return helpRunner{path, &helpConfig{
    33  		style:     env.style(),
    34  		width:     env.width(),
    35  		prefix:    env.prefix(),
    36  		firstCall: env.firstCall(),
    37  	}}
    38  }
    39  
    40  // helpConfig holds configuration data for help.  The style and width may be
    41  // overriden by flags if the command returned by newCommand is parsed.
    42  type helpConfig struct {
    43  	style     style
    44  	width     int
    45  	prefix    string
    46  	firstCall bool
    47  }
    48  
    49  // Run implements the Runner interface method.
    50  func (h helpRunner) Run(env *Env, args []string) error {
    51  	w := textutil.NewUTF8WrapWriter(env.Stdout, h.width)
    52  	defer w.Flush()
    53  	return runHelp(w, env, args, h.path, h.helpConfig)
    54  }
    55  
    56  // usageFunc is used as the implementation of the Env.Usage function.
    57  func (h helpRunner) usageFunc(env *Env, writer io.Writer) {
    58  	w := textutil.NewUTF8WrapWriter(writer, h.width)
    59  	usage(w, env, h.path, h.helpConfig, h.helpConfig.firstCall)
    60  	w.Flush()
    61  }
    62  
    63  const (
    64  	helpName  = "help"
    65  	helpShort = "Display help for commands or topics"
    66  )
    67  
    68  // newCommand returns a new help command that uses h as its Runner.
    69  func (h helpRunner) newCommand() *Command {
    70  	help := &Command{
    71  		Runner: h,
    72  		Name:   helpName,
    73  		Short:  helpShort,
    74  		Long: `
    75  Help with no args displays the usage of the parent command.
    76  
    77  Help with args displays the usage of the specified sub-command or help topic.
    78  
    79  "help ..." recursively displays help for all commands and topics.
    80  `,
    81  		ArgsName: "[command/topic ...]",
    82  		ArgsLong: `
    83  [command/topic ...] optionally identifies a specific sub-command or help topic.
    84  `,
    85  	}
    86  	help.Flags.Var(&h.style, "style", `
    87  The formatting style for help output:
    88     compact   - Good for compact cmdline output.
    89     full      - Good for cmdline output, shows all global flags.
    90     godoc     - Good for godoc processing.
    91     shortonly - Only output short description.
    92  Override the default by setting the CMDLINE_STYLE environment variable.
    93  `)
    94  	help.Flags.IntVar(&h.width, "width", h.width, `
    95  Format output to this target width in runes, or unlimited if width < 0.
    96  Defaults to the terminal width if available.  Override the default by setting
    97  the CMDLINE_WIDTH environment variable.
    98  `)
    99  	// Override default values, so that the godoc style shows good defaults.
   100  	help.Flags.Lookup("style").DefValue = "compact"
   101  	help.Flags.Lookup("width").DefValue = "<terminal width>"
   102  	cleanTree(help)
   103  	return help
   104  }
   105  
   106  // runHelp implements the run-time behavior of the help command.
   107  func runHelp(w *textutil.WrapWriter, env *Env, args []string, path []*Command, config *helpConfig) error {
   108  	if len(args) == 0 {
   109  		usage(w, env, path, config, config.firstCall)
   110  		return nil
   111  	}
   112  	if args[0] == "..." {
   113  		usageAll(w, env, path, config, config.firstCall)
   114  		return nil
   115  	}
   116  	// Look for matching children.
   117  	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
   118  	subName, subArgs := args[0], args[1:]
   119  	for _, child := range cmd.Children {
   120  		if child.Name == subName {
   121  			return runHelp(w, env, subArgs, append(path, child), config)
   122  		}
   123  	}
   124  	if helpName == subName {
   125  		help := helpRunner{path, config}.newCommand()
   126  		return runHelp(w, env, subArgs, append(path, help), config)
   127  	}
   128  	if cmd.LookPath {
   129  		// Look for a matching executable in PATH.
   130  		if subCmd, _ := env.LookPath(cmd.Name + "-" + subName); subCmd != "" {
   131  			runner := binaryRunner{subCmd, cmdPath}
   132  			envCopy := env.clone()
   133  			envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
   134  			if len(subArgs) == 0 {
   135  				return runner.Run(envCopy, []string{"-help"})
   136  			}
   137  			return runner.Run(envCopy, append([]string{helpName}, subArgs...))
   138  		}
   139  	}
   140  	// Look for matching topic.
   141  	for _, topic := range cmd.Topics {
   142  		if topic.Name == subName {
   143  			fmt.Fprintln(w, topic.Long)
   144  			return nil
   145  		}
   146  	}
   147  	fn := helpRunner{path, config}.usageFunc
   148  	return usageErrorf(env, fn, "%s: unknown command or topic %q", cmdPath, subName)
   149  }
   150  
   151  func godocHeader(path, short string) string {
   152  	// The first rune must be uppercase for godoc to recognize the string as a
   153  	// section header, which is linked to the table of contents.
   154  	switch {
   155  	case path == "":
   156  		return firstRuneToUpper(short)
   157  	case short == "":
   158  		return firstRuneToUpper(path)
   159  	}
   160  	// Godoc has special heurisitics to extract headers from the comments, from
   161  	// which it builds a nice table of contents.  Headers must be single
   162  	// unindented lines with unindented paragraphs both before and after, and the
   163  	// line must not include certain characters.
   164  	//
   165  	// We try our best to create a header that includes both the command path and
   166  	// the short description, but if godoc won't extract a header out of the line,
   167  	// we fall back to just returning the command path.
   168  	//
   169  	// For more details see the comments and implementation of doc.ToHTML:
   170  	// http://golang.org/pkg/go/doc/#ToHTML
   171  	header := firstRuneToUpper(path + " - " + short)
   172  	var buf bytes.Buffer
   173  	doc.ToHTML(&buf, "before\n\n"+header+"\n\nafter", nil)
   174  	if !bytes.Contains(buf.Bytes(), []byte("<h")) {
   175  		return firstRuneToUpper(path)
   176  	}
   177  	return header
   178  }
   179  
   180  func firstRuneToUpper(s string) string {
   181  	if s == "" {
   182  		return ""
   183  	}
   184  	r, n := utf8.DecodeRuneInString(s)
   185  	return string(unicode.ToUpper(r)) + s[n:]
   186  }
   187  
   188  func lineBreak(w *textutil.WrapWriter, style style) {
   189  	w.Flush()
   190  	switch style {
   191  	case styleCompact, styleFull:
   192  		width := w.Width()
   193  		if width < 0 {
   194  			// If the user has chosen an "unlimited" word-wrapping width, we still
   195  			// need a reasonable width for our visual line break.
   196  			width = defaultWidth
   197  		}
   198  		fmt.Fprintln(w, strings.Repeat("=", width))
   199  	case styleGoDoc:
   200  		fmt.Fprintln(w)
   201  	}
   202  	w.Flush()
   203  }
   204  
   205  // needsHelpChild returns true if cmd needs a default help command to be
   206  // appended to its children.  Every command that has children and doesn't
   207  // already have a "help" command needs a help child.
   208  func needsHelpChild(cmd *Command) bool {
   209  	for _, child := range cmd.Children {
   210  		if child.Name == helpName {
   211  			return false
   212  		}
   213  	}
   214  	return len(cmd.Children) > 0
   215  }
   216  
   217  // usageAll prints usage recursively via DFS from the path onward.
   218  func usageAll(w *textutil.WrapWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
   219  	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
   220  	usage(w, env, path, config, firstCall)
   221  	for _, child := range cmd.Children {
   222  		usageAll(w, env, append(path, child), config, false)
   223  	}
   224  	if firstCall && needsHelpChild(cmd) {
   225  		help := helpRunner{path, config}.newCommand()
   226  		usageAll(w, env, append(path, help), config, false)
   227  	}
   228  	if cmd.LookPath {
   229  		cmdPrefix := cmd.Name + "-"
   230  		subCmds, _ := env.LookPathPrefix(cmdPrefix, cmd.subNames(cmdPrefix))
   231  		for _, subCmd := range subCmds {
   232  			runner := binaryRunner{subCmd, cmdPath}
   233  			var buffer bytes.Buffer
   234  			envCopy := env.clone()
   235  			envCopy.Stdout = &buffer
   236  			envCopy.Stderr = &buffer
   237  			envCopy.Vars["CMDLINE_FIRST_CALL"] = "false"
   238  			envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
   239  			if err := runner.Run(envCopy, []string{helpName, "..."}); err == nil {
   240  				// The external child supports "help".
   241  				if config.style == styleGoDoc {
   242  					// The textutil package will discard any leading empty lines
   243  					// produced by the child process output, so we need to
   244  					// output it here.
   245  					fmt.Fprintln(w)
   246  				}
   247  				fmt.Fprint(w, buffer.String())
   248  				continue
   249  			}
   250  			buffer.Reset()
   251  			if err := runner.Run(envCopy, []string{"-help"}); err == nil {
   252  				// The external child supports "-help".
   253  				if config.style == styleGoDoc {
   254  					// The textutil package will discard any leading empty lines
   255  					// produced by the child process output, so we need to
   256  					// output it here.
   257  					fmt.Fprintln(w)
   258  				}
   259  				fmt.Fprint(w, buffer.String())
   260  				continue
   261  			}
   262  			// The external child does not support "help" or "-help".
   263  			lineBreak(w, config.style)
   264  			subName := strings.TrimPrefix(filepath.Base(subCmd), cmdPrefix)
   265  			fmt.Fprintln(w, godocHeader(cmdPath+" "+subName, missingDescription))
   266  		}
   267  	}
   268  	for _, topic := range cmd.Topics {
   269  		lineBreak(w, config.style)
   270  		w.ForceVerbatim(true)
   271  		fmt.Fprintln(w, godocHeader(cmdPath+" "+topic.Name, topic.Short))
   272  		w.ForceVerbatim(false)
   273  		fmt.Fprintln(w)
   274  		fmt.Fprintln(w, topic.Long)
   275  	}
   276  }
   277  
   278  // usage prints the usage of the last command in path to w.  The bool firstCall
   279  // is set to false when printing usage for multiple commands, and is used to
   280  // avoid printing redundant information (e.g. help command, global flags).
   281  func usage(w *textutil.WrapWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
   282  	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
   283  	env.TimerPush("usage " + cmdPath)
   284  	defer env.TimerPop()
   285  	if config.style == styleShortOnly {
   286  		fmt.Fprintln(w, cmd.Short)
   287  		return
   288  	}
   289  	if !firstCall {
   290  		lineBreak(w, config.style)
   291  		w.ForceVerbatim(true)
   292  		fmt.Fprintln(w, godocHeader(cmdPath, cmd.Short))
   293  		w.ForceVerbatim(false)
   294  		fmt.Fprintln(w)
   295  	}
   296  	fmt.Fprintln(w, cmd.Long)
   297  	fmt.Fprintln(w)
   298  	// Usage line.
   299  	fmt.Fprintln(w, "Usage:")
   300  	cmdPathF := "   " + cmdPath
   301  	if countFlags(pathFlags(path), nil, true) > 0 || countFlags(globalFlags, nil, true) > 0 {
   302  		cmdPathF += " [flags]"
   303  	}
   304  	if cmd.Runner != nil {
   305  		if cmd.ArgsName != "" {
   306  			fmt.Fprintln(w, cmdPathF, cmd.ArgsName)
   307  		} else {
   308  			fmt.Fprintln(w, cmdPathF)
   309  		}
   310  	}
   311  	var extChildren []string
   312  	cmdPrefix := cmd.Name + "-"
   313  	if cmd.LookPath {
   314  		extChildren, _ = env.LookPathPrefix(cmdPrefix, cmd.subNames(cmdPrefix))
   315  	}
   316  	hasSubcommands := len(cmd.Children) > 0 || len(extChildren) > 0
   317  	if hasSubcommands {
   318  		fmt.Fprintln(w, cmdPathF, "<command>")
   319  		fmt.Fprintln(w)
   320  	}
   321  	printShort := func(width int, name, short string) {
   322  		fmt.Fprintf(w, "%-[1]*[2]s %[3]s", width, name, short)
   323  		w.Flush()
   324  	}
   325  	const minNameWidth = 11
   326  	nameWidth := minNameWidth
   327  	for _, child := range cmd.Children {
   328  		if w := len(child.Name); w > nameWidth {
   329  			nameWidth = w
   330  		}
   331  	}
   332  	for _, extCmd := range extChildren {
   333  		extName := strings.TrimPrefix(filepath.Base(extCmd), cmdPrefix)
   334  		if w := len(extName); w > nameWidth {
   335  			nameWidth = w
   336  		}
   337  	}
   338  	// Built-in commands.
   339  	if len(cmd.Children) > 0 {
   340  		w.SetIndents()
   341  		fmt.Fprintln(w, "The", cmdPath, "commands are:")
   342  		// Print as a table with aligned columns Name and Short.
   343  		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
   344  		for _, child := range cmd.Children {
   345  			printShort(nameWidth, child.Name, child.Short)
   346  		}
   347  		// Default help command.
   348  		if firstCall && needsHelpChild(cmd) {
   349  			printShort(nameWidth, helpName, helpShort)
   350  		}
   351  	}
   352  	// External commands.
   353  	if len(extChildren) > 0 {
   354  		w.SetIndents()
   355  		fmt.Fprintln(w, "The", cmdPath, "external commands are:")
   356  		// Print as a table with aligned columns Name and Short.
   357  		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
   358  		for _, extCmd := range extChildren {
   359  			runner := binaryRunner{extCmd, cmdPath}
   360  			var buffer bytes.Buffer
   361  			envCopy := env.clone()
   362  			envCopy.Stdout = &buffer
   363  			envCopy.Stderr = &buffer
   364  			envCopy.Vars["CMDLINE_STYLE"] = "shortonly"
   365  			short := missingDescription
   366  			if err := runner.Run(envCopy, []string{"-help"}); err == nil {
   367  				// The external child supports "-help".
   368  				short = buffer.String()
   369  			}
   370  			extName := strings.TrimPrefix(filepath.Base(extCmd), cmdPrefix)
   371  			printShort(nameWidth, extName, short)
   372  		}
   373  	}
   374  	// Command footer.
   375  	if hasSubcommands {
   376  		w.SetIndents()
   377  		if firstCall && config.style != styleGoDoc {
   378  			fmt.Fprintf(w, "Run \"%s help [command]\" for command usage.\n", cmdPath)
   379  		}
   380  	}
   381  	// Args.
   382  	if cmd.Runner != nil && cmd.ArgsLong != "" {
   383  		fmt.Fprintln(w)
   384  		fmt.Fprintln(w, cmd.ArgsLong)
   385  	}
   386  	// Help topics.
   387  	if len(cmd.Topics) > 0 {
   388  		fmt.Fprintln(w)
   389  		fmt.Fprintln(w, "The", cmdPath, "additional help topics are:")
   390  		nameWidth := minNameWidth
   391  		for _, topic := range cmd.Topics {
   392  			if w := len(topic.Name); w > nameWidth {
   393  				nameWidth = w
   394  			}
   395  		}
   396  		// Print as a table with aligned columns Name and Short.
   397  		w.SetIndents(spaces(3), spaces(3+nameWidth+1))
   398  		for _, topic := range cmd.Topics {
   399  			printShort(nameWidth, topic.Name, topic.Short)
   400  		}
   401  		w.SetIndents()
   402  		if firstCall && config.style != styleGoDoc {
   403  			fmt.Fprintf(w, "Run \"%s help [topic]\" for topic details.\n", cmdPath)
   404  		}
   405  	}
   406  	hidden := flagsUsage(w, path, config)
   407  	// Only show global flags on the first call.
   408  	if firstCall {
   409  		hidden = globalFlagsUsage(w, config) || hidden
   410  	}
   411  	if hidden {
   412  		fmt.Fprintln(w)
   413  		fullhelp := fmt.Sprintf(`Run "%s help -style=full" to show all flags.`, cmdPath)
   414  		if len(cmd.Children) == 0 {
   415  			if len(path) > 1 {
   416  				parentPath := pathName(config.prefix, path[:len(path)-1])
   417  				fullhelp = fmt.Sprintf(`Run "%s help -style=full %s" to show all flags.`, parentPath, cmd.Name)
   418  			} else {
   419  				fullhelp = fmt.Sprintf(`Run "CMDLINE_STYLE=full %s -help" to show all flags.`, cmdPath)
   420  			}
   421  		}
   422  		fmt.Fprintln(w, fullhelp)
   423  	}
   424  }
   425  
   426  func flagsUsage(w *textutil.WrapWriter, path []*Command, config *helpConfig) bool {
   427  	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
   428  	allFlags := pathFlags(path)
   429  	numCompact := countFlags(&cmd.Flags, nil, true)
   430  	numFull := countFlags(allFlags, nil, true) - numCompact
   431  	if config.style == styleCompact {
   432  		// Compact style, only show compact flags.
   433  		if numCompact > 0 {
   434  			fmt.Fprintln(w)
   435  			fmt.Fprintln(w, "The", cmdPath, "flags are:")
   436  			printFlags(w, &cmd.Flags, nil, config.style, nil, true)
   437  		}
   438  		return numFull > 0
   439  	}
   440  	// Non-compact style, always show all flags.
   441  	if numCompact > 0 || numFull > 0 {
   442  		fmt.Fprintln(w)
   443  		fmt.Fprintln(w, "The", cmdPath, "flags are:")
   444  		printFlags(w, &cmd.Flags, nil, config.style, nil, true)
   445  		if numCompact > 0 && numFull > 0 {
   446  			fmt.Fprintln(w)
   447  		}
   448  		printFlags(w, allFlags, &cmd.Flags, config.style, nil, true)
   449  	}
   450  	return false
   451  }
   452  
   453  func globalFlagsUsage(w *textutil.WrapWriter, config *helpConfig) bool {
   454  	regex := regexp.MustCompilePOSIX("^test\\..*$")
   455  	HideGlobalFlags(regex)
   456  	numCompact := countFlags(globalFlags, hiddenGlobalFlags, false)
   457  	numFull := countFlags(globalFlags, hiddenGlobalFlags, true)
   458  	if config.style == styleCompact {
   459  		// Compact style, only show compact flags.
   460  		if numCompact > 0 {
   461  			fmt.Fprintln(w)
   462  			fmt.Fprintln(w, "The global flags are:")
   463  			printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, false)
   464  		}
   465  		return numFull > 0
   466  	}
   467  	// Non-compact style, always show all global flags.
   468  	if numCompact > 0 || numFull > 0 {
   469  		fmt.Fprintln(w)
   470  		fmt.Fprintln(w, "The global flags are:")
   471  		printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, false)
   472  		if numCompact > 0 && numFull > 0 {
   473  			fmt.Fprintln(w)
   474  		}
   475  		printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, true)
   476  	}
   477  	return false
   478  }
   479  
   480  func countFlags(flags *flag.FlagSet, regexps []*regexp.Regexp, match bool) (num int) {
   481  	flags.VisitAll(func(f *flag.Flag) {
   482  		if match == matchRegexps(regexps, f.Name) {
   483  			num++
   484  		}
   485  	})
   486  	return
   487  }
   488  
   489  func printFlags(w *textutil.WrapWriter, flags, filter *flag.FlagSet, style style, regexps []*regexp.Regexp, match bool) {
   490  	flags.VisitAll(func(f *flag.Flag) {
   491  		if filter != nil && filter.Lookup(f.Name) != nil {
   492  			return
   493  		}
   494  		if match != matchRegexps(regexps, f.Name) {
   495  			return
   496  		}
   497  		value := f.Value.String()
   498  		if style == styleGoDoc {
   499  			// When using styleGoDoc we use the default value, so that e.g. regular
   500  			// help will show "/usr/home/me/foo" while godoc will show "$HOME/foo".
   501  			value = f.DefValue
   502  		}
   503  		fmt.Fprintf(w, " -%s=%v", f.Name, value)
   504  		w.SetIndents(spaces(3))
   505  		fmt.Fprintln(w, f.Usage)
   506  		w.SetIndents()
   507  	})
   508  }
   509  
   510  func spaces(count int) string {
   511  	return strings.Repeat(" ", count)
   512  }
   513  
   514  func matchRegexps(regexps []*regexp.Regexp, name string) bool {
   515  	// We distinguish nil regexps from empty regexps; the former means "all names
   516  	// match", while the latter means "no names match".
   517  	if regexps == nil {
   518  		return true
   519  	}
   520  	for _, r := range regexps {
   521  		if r.MatchString(name) {
   522  			return true
   523  		}
   524  	}
   525  	return false
   526  }
   527  
   528  var hiddenGlobalFlags []*regexp.Regexp
   529  
   530  // HideGlobalFlags hides global flags from the default compact-style usage
   531  // message.  Global flag names that match any of the regexps will not be shown
   532  // in the compact usage message.  Multiple calls behave as if all regexps were
   533  // provided in a single call.
   534  //
   535  // All global flags are always shown in non-compact style usage messages.
   536  func HideGlobalFlags(regexps ...*regexp.Regexp) {
   537  	hiddenGlobalFlags = append(hiddenGlobalFlags, regexps...)
   538  	if hiddenGlobalFlags == nil {
   539  		hiddenGlobalFlags = []*regexp.Regexp{}
   540  	}
   541  }