...

Source file src/github.com/urfave/cli/v2/help.go

Documentation: github.com/urfave/cli/v2

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"strings"
     8  	"text/tabwriter"
     9  	"text/template"
    10  	"unicode/utf8"
    11  )
    12  
    13  const (
    14  	helpName  = "help"
    15  	helpAlias = "h"
    16  )
    17  
    18  // this instance is to avoid recursion in the ShowCommandHelp which can
    19  // add a help command again
    20  var helpCommandDontUse = &Command{
    21  	Name:      helpName,
    22  	Aliases:   []string{helpAlias},
    23  	Usage:     "Shows a list of commands or help for one command",
    24  	ArgsUsage: "[command]",
    25  }
    26  
    27  var helpCommand = &Command{
    28  	Name:      helpName,
    29  	Aliases:   []string{helpAlias},
    30  	Usage:     "Shows a list of commands or help for one command",
    31  	ArgsUsage: "[command]",
    32  	Action: func(cCtx *Context) error {
    33  		args := cCtx.Args()
    34  		argsPresent := args.First() != ""
    35  		firstArg := args.First()
    36  
    37  		// This action can be triggered by a "default" action of a command
    38  		// or via cmd.Run when cmd == helpCmd. So we have following possibilities
    39  		//
    40  		// 1 $ app
    41  		// 2 $ app help
    42  		// 3 $ app foo
    43  		// 4 $ app help foo
    44  		// 5 $ app foo help
    45  		// 6 $ app foo -h (with no other sub-commands nor flags defined)
    46  
    47  		// Case 4. when executing a help command set the context to parent
    48  		// to allow resolution of subsequent args. This will transform
    49  		// $ app help foo
    50  		//     to
    51  		// $ app foo
    52  		// which will then be handled as case 3
    53  		if cCtx.Command.Name == helpName || cCtx.Command.Name == helpAlias {
    54  			cCtx = cCtx.parentContext
    55  		}
    56  
    57  		// Case 4. $ app hello foo
    58  		// foo is the command for which help needs to be shown
    59  		if argsPresent {
    60  			return ShowCommandHelp(cCtx, firstArg)
    61  		}
    62  
    63  		// Case 1 & 2
    64  		// Special case when running help on main app itself as opposed to individual
    65  		// commands/subcommands
    66  		if cCtx.parentContext.App == nil {
    67  			_ = ShowAppHelp(cCtx)
    68  			return nil
    69  		}
    70  
    71  		// Case 3, 5
    72  		if (len(cCtx.Command.Subcommands) == 1 && !cCtx.Command.HideHelp && !cCtx.Command.HideHelpCommand) ||
    73  			(len(cCtx.Command.Subcommands) == 0 && cCtx.Command.HideHelp) {
    74  			templ := cCtx.Command.CustomHelpTemplate
    75  			if templ == "" {
    76  				templ = CommandHelpTemplate
    77  			}
    78  			HelpPrinter(cCtx.App.Writer, templ, cCtx.Command)
    79  			return nil
    80  		}
    81  
    82  		// Case 6, handling incorporated in the callee itself
    83  		return ShowSubcommandHelp(cCtx)
    84  	},
    85  }
    86  
    87  // Prints help for the App or Command
    88  type helpPrinter func(w io.Writer, templ string, data interface{})
    89  
    90  // Prints help for the App or Command with custom template function.
    91  type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
    92  
    93  // HelpPrinter is a function that writes the help output. If not set explicitly,
    94  // this calls HelpPrinterCustom using only the default template functions.
    95  //
    96  // If custom logic for printing help is required, this function can be
    97  // overridden. If the ExtraInfo field is defined on an App, this function
    98  // should not be modified, as HelpPrinterCustom will be used directly in order
    99  // to capture the extra information.
   100  var HelpPrinter helpPrinter = printHelp
   101  
   102  // HelpPrinterCustom is a function that writes the help output. It is used as
   103  // the default implementation of HelpPrinter, and may be called directly if
   104  // the ExtraInfo field is set on an App.
   105  //
   106  // In the default implementation, if the customFuncs argument contains a
   107  // "wrapAt" key, which is a function which takes no arguments and returns
   108  // an int, this int value will be used to produce a "wrap" function used
   109  // by the default template to wrap long lines.
   110  var HelpPrinterCustom helpPrinterCustom = printHelpCustom
   111  
   112  // VersionPrinter prints the version for the App
   113  var VersionPrinter = printVersion
   114  
   115  // ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code.
   116  func ShowAppHelpAndExit(c *Context, exitCode int) {
   117  	_ = ShowAppHelp(c)
   118  	os.Exit(exitCode)
   119  }
   120  
   121  // ShowAppHelp is an action that displays the help.
   122  func ShowAppHelp(cCtx *Context) error {
   123  	tpl := cCtx.App.CustomAppHelpTemplate
   124  	if tpl == "" {
   125  		tpl = AppHelpTemplate
   126  	}
   127  
   128  	if cCtx.App.ExtraInfo == nil {
   129  		HelpPrinter(cCtx.App.Writer, tpl, cCtx.App)
   130  		return nil
   131  	}
   132  
   133  	customAppData := func() map[string]interface{} {
   134  		return map[string]interface{}{
   135  			"ExtraInfo": cCtx.App.ExtraInfo,
   136  		}
   137  	}
   138  	HelpPrinterCustom(cCtx.App.Writer, tpl, cCtx.App, customAppData())
   139  
   140  	return nil
   141  }
   142  
   143  // DefaultAppComplete prints the list of subcommands as the default app completion method
   144  func DefaultAppComplete(cCtx *Context) {
   145  	DefaultCompleteWithFlags(nil)(cCtx)
   146  }
   147  
   148  func printCommandSuggestions(commands []*Command, writer io.Writer) {
   149  	for _, command := range commands {
   150  		if command.Hidden {
   151  			continue
   152  		}
   153  		if strings.HasSuffix(os.Getenv("SHELL"), "zsh") {
   154  			for _, name := range command.Names() {
   155  				_, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
   156  			}
   157  		} else {
   158  			for _, name := range command.Names() {
   159  				_, _ = fmt.Fprintf(writer, "%s\n", name)
   160  			}
   161  		}
   162  	}
   163  }
   164  
   165  func cliArgContains(flagName string) bool {
   166  	for _, name := range strings.Split(flagName, ",") {
   167  		name = strings.TrimSpace(name)
   168  		count := utf8.RuneCountInString(name)
   169  		if count > 2 {
   170  			count = 2
   171  		}
   172  		flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
   173  		for _, a := range os.Args {
   174  			if a == flag {
   175  				return true
   176  			}
   177  		}
   178  	}
   179  	return false
   180  }
   181  
   182  func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
   183  	cur := strings.TrimPrefix(lastArg, "-")
   184  	cur = strings.TrimPrefix(cur, "-")
   185  	for _, flag := range flags {
   186  		if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden {
   187  			continue
   188  		}
   189  		for _, name := range flag.Names() {
   190  			name = strings.TrimSpace(name)
   191  			// this will get total count utf8 letters in flag name
   192  			count := utf8.RuneCountInString(name)
   193  			if count > 2 {
   194  				count = 2 // reuse this count to generate single - or -- in flag completion
   195  			}
   196  			// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
   197  			// skip flag completion for short flags example -v or -x
   198  			if strings.HasPrefix(lastArg, "--") && count == 1 {
   199  				continue
   200  			}
   201  			// match if last argument matches this flag and it is not repeated
   202  			if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) {
   203  				flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
   204  				_, _ = fmt.Fprintln(writer, flagCompletion)
   205  			}
   206  		}
   207  	}
   208  }
   209  
   210  func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) {
   211  	return func(cCtx *Context) {
   212  		var lastArg string
   213  
   214  		// TODO: This shouldnt depend on os.Args rather it should
   215  		// depend on root arguments passed to App
   216  		if len(os.Args) > 2 {
   217  			lastArg = os.Args[len(os.Args)-2]
   218  		}
   219  
   220  		if lastArg != "" {
   221  			if strings.HasPrefix(lastArg, "-") {
   222  				if cmd != nil {
   223  					printFlagSuggestions(lastArg, cmd.Flags, cCtx.App.Writer)
   224  
   225  					return
   226  				}
   227  
   228  				printFlagSuggestions(lastArg, cCtx.App.Flags, cCtx.App.Writer)
   229  
   230  				return
   231  			}
   232  		}
   233  
   234  		if cmd != nil {
   235  			printCommandSuggestions(cmd.Subcommands, cCtx.App.Writer)
   236  			return
   237  		}
   238  
   239  		printCommandSuggestions(cCtx.Command.Subcommands, cCtx.App.Writer)
   240  	}
   241  }
   242  
   243  // ShowCommandHelpAndExit - exits with code after showing help
   244  func ShowCommandHelpAndExit(c *Context, command string, code int) {
   245  	_ = ShowCommandHelp(c, command)
   246  	os.Exit(code)
   247  }
   248  
   249  // ShowCommandHelp prints help for the given command
   250  func ShowCommandHelp(ctx *Context, command string) error {
   251  
   252  	commands := ctx.App.Commands
   253  	if ctx.Command.Subcommands != nil {
   254  		commands = ctx.Command.Subcommands
   255  	}
   256  	for _, c := range commands {
   257  		if c.HasName(command) {
   258  			if !ctx.App.HideHelpCommand && !c.HasName(helpName) && len(c.Subcommands) != 0 && c.Command(helpName) == nil {
   259  				c.Subcommands = append(c.Subcommands, helpCommandDontUse)
   260  			}
   261  			if !ctx.App.HideHelp && HelpFlag != nil {
   262  				c.appendFlag(HelpFlag)
   263  			}
   264  			templ := c.CustomHelpTemplate
   265  			if templ == "" {
   266  				if len(c.Subcommands) == 0 {
   267  					templ = CommandHelpTemplate
   268  				} else {
   269  					templ = SubcommandHelpTemplate
   270  				}
   271  			}
   272  
   273  			HelpPrinter(ctx.App.Writer, templ, c)
   274  
   275  			return nil
   276  		}
   277  	}
   278  
   279  	if ctx.App.CommandNotFound == nil {
   280  		errMsg := fmt.Sprintf("No help topic for '%v'", command)
   281  		if ctx.App.Suggest && SuggestCommand != nil {
   282  			if suggestion := SuggestCommand(ctx.Command.Subcommands, command); suggestion != "" {
   283  				errMsg += ". " + suggestion
   284  			}
   285  		}
   286  		return Exit(errMsg, 3)
   287  	}
   288  
   289  	ctx.App.CommandNotFound(ctx, command)
   290  	return nil
   291  }
   292  
   293  // ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code.
   294  func ShowSubcommandHelpAndExit(c *Context, exitCode int) {
   295  	_ = ShowSubcommandHelp(c)
   296  	os.Exit(exitCode)
   297  }
   298  
   299  // ShowSubcommandHelp prints help for the given subcommand
   300  func ShowSubcommandHelp(cCtx *Context) error {
   301  	if cCtx == nil {
   302  		return nil
   303  	}
   304  	// use custom template when provided (fixes #1703)
   305  	templ := SubcommandHelpTemplate
   306  	if cCtx.Command != nil && cCtx.Command.CustomHelpTemplate != "" {
   307  		templ = cCtx.Command.CustomHelpTemplate
   308  	}
   309  	HelpPrinter(cCtx.App.Writer, templ, cCtx.Command)
   310  	return nil
   311  }
   312  
   313  // ShowVersion prints the version number of the App
   314  func ShowVersion(cCtx *Context) {
   315  	VersionPrinter(cCtx)
   316  }
   317  
   318  func printVersion(cCtx *Context) {
   319  	_, _ = fmt.Fprintf(cCtx.App.Writer, "%v version %v\n", cCtx.App.Name, cCtx.App.Version)
   320  }
   321  
   322  // ShowCompletions prints the lists of commands within a given context
   323  func ShowCompletions(cCtx *Context) {
   324  	c := cCtx.Command
   325  	if c != nil && c.BashComplete != nil {
   326  		c.BashComplete(cCtx)
   327  	}
   328  }
   329  
   330  // ShowCommandCompletions prints the custom completions for a given command
   331  func ShowCommandCompletions(ctx *Context, command string) {
   332  	c := ctx.Command.Command(command)
   333  	if c != nil {
   334  		if c.BashComplete != nil {
   335  			c.BashComplete(ctx)
   336  		} else {
   337  			DefaultCompleteWithFlags(c)(ctx)
   338  		}
   339  	}
   340  
   341  }
   342  
   343  // printHelpCustom is the default implementation of HelpPrinterCustom.
   344  //
   345  // The customFuncs map will be combined with a default template.FuncMap to
   346  // allow using arbitrary functions in template rendering.
   347  func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
   348  
   349  	const maxLineLength = 10000
   350  
   351  	funcMap := template.FuncMap{
   352  		"join":           strings.Join,
   353  		"subtract":       subtract,
   354  		"indent":         indent,
   355  		"nindent":        nindent,
   356  		"trim":           strings.TrimSpace,
   357  		"wrap":           func(input string, offset int) string { return wrap(input, offset, maxLineLength) },
   358  		"offset":         offset,
   359  		"offsetCommands": offsetCommands,
   360  	}
   361  
   362  	if customFuncs["wrapAt"] != nil {
   363  		if wa, ok := customFuncs["wrapAt"]; ok {
   364  			if waf, ok := wa.(func() int); ok {
   365  				wrapAt := waf()
   366  				customFuncs["wrap"] = func(input string, offset int) string {
   367  					return wrap(input, offset, wrapAt)
   368  				}
   369  			}
   370  		}
   371  	}
   372  
   373  	for key, value := range customFuncs {
   374  		funcMap[key] = value
   375  	}
   376  
   377  	w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
   378  	t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
   379  	templates := map[string]string{
   380  		"helpNameTemplate":                  helpNameTemplate,
   381  		"usageTemplate":                     usageTemplate,
   382  		"descriptionTemplate":               descriptionTemplate,
   383  		"visibleCommandTemplate":            visibleCommandTemplate,
   384  		"copyrightTemplate":                 copyrightTemplate,
   385  		"versionTemplate":                   versionTemplate,
   386  		"visibleFlagCategoryTemplate":       visibleFlagCategoryTemplate,
   387  		"visibleFlagTemplate":               visibleFlagTemplate,
   388  		"visibleGlobalFlagCategoryTemplate": strings.Replace(visibleFlagCategoryTemplate, "OPTIONS", "GLOBAL OPTIONS", -1),
   389  		"authorsTemplate":                   authorsTemplate,
   390  		"visibleCommandCategoryTemplate":    visibleCommandCategoryTemplate,
   391  	}
   392  	for name, value := range templates {
   393  		if _, err := t.New(name).Parse(value); err != nil {
   394  			if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
   395  				_, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
   396  			}
   397  		}
   398  	}
   399  
   400  	err := t.Execute(w, data)
   401  	if err != nil {
   402  		// If the writer is closed, t.Execute will fail, and there's nothing
   403  		// we can do to recover.
   404  		if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
   405  			_, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
   406  		}
   407  		return
   408  	}
   409  	_ = w.Flush()
   410  }
   411  
   412  func printHelp(out io.Writer, templ string, data interface{}) {
   413  	HelpPrinterCustom(out, templ, data, nil)
   414  }
   415  
   416  func checkVersion(cCtx *Context) bool {
   417  	found := false
   418  	for _, name := range VersionFlag.Names() {
   419  		if cCtx.Bool(name) {
   420  			found = true
   421  		}
   422  	}
   423  	return found
   424  }
   425  
   426  func checkHelp(cCtx *Context) bool {
   427  	if HelpFlag == nil {
   428  		return false
   429  	}
   430  	found := false
   431  	for _, name := range HelpFlag.Names() {
   432  		if cCtx.Bool(name) {
   433  			found = true
   434  			break
   435  		}
   436  	}
   437  
   438  	return found
   439  }
   440  
   441  func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
   442  	if !a.EnableBashCompletion {
   443  		return false, arguments
   444  	}
   445  
   446  	pos := len(arguments) - 1
   447  	lastArg := arguments[pos]
   448  
   449  	if lastArg != "--generate-bash-completion" {
   450  		return false, arguments
   451  	}
   452  
   453  	return true, arguments[:pos]
   454  }
   455  
   456  func checkCompletions(cCtx *Context) bool {
   457  	if !cCtx.shellComplete {
   458  		return false
   459  	}
   460  
   461  	if args := cCtx.Args(); args.Present() {
   462  		name := args.First()
   463  		if cmd := cCtx.Command.Command(name); cmd != nil {
   464  			// let the command handle the completion
   465  			return false
   466  		}
   467  	}
   468  
   469  	ShowCompletions(cCtx)
   470  	return true
   471  }
   472  
   473  func subtract(a, b int) int {
   474  	return a - b
   475  }
   476  
   477  func indent(spaces int, v string) string {
   478  	pad := strings.Repeat(" ", spaces)
   479  	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
   480  }
   481  
   482  func nindent(spaces int, v string) string {
   483  	return "\n" + indent(spaces, v)
   484  }
   485  
   486  func wrap(input string, offset int, wrapAt int) string {
   487  	var ss []string
   488  
   489  	lines := strings.Split(input, "\n")
   490  
   491  	padding := strings.Repeat(" ", offset)
   492  
   493  	for i, line := range lines {
   494  		if line == "" {
   495  			ss = append(ss, line)
   496  		} else {
   497  			wrapped := wrapLine(line, offset, wrapAt, padding)
   498  			if i == 0 {
   499  				ss = append(ss, wrapped)
   500  			} else {
   501  				ss = append(ss, padding+wrapped)
   502  
   503  			}
   504  
   505  		}
   506  	}
   507  
   508  	return strings.Join(ss, "\n")
   509  }
   510  
   511  func wrapLine(input string, offset int, wrapAt int, padding string) string {
   512  	if wrapAt <= offset || len(input) <= wrapAt-offset {
   513  		return input
   514  	}
   515  
   516  	lineWidth := wrapAt - offset
   517  	words := strings.Fields(input)
   518  	if len(words) == 0 {
   519  		return input
   520  	}
   521  
   522  	wrapped := words[0]
   523  	spaceLeft := lineWidth - len(wrapped)
   524  	for _, word := range words[1:] {
   525  		if len(word)+1 > spaceLeft {
   526  			wrapped += "\n" + padding + word
   527  			spaceLeft = lineWidth - len(word)
   528  		} else {
   529  			wrapped += " " + word
   530  			spaceLeft -= 1 + len(word)
   531  		}
   532  	}
   533  
   534  	return wrapped
   535  }
   536  
   537  func offset(input string, fixed int) int {
   538  	return len(input) + fixed
   539  }
   540  
   541  // this function tries to find the max width of the names column
   542  // so say we have the following rows for help
   543  //
   544  //	foo1, foo2, foo3  some string here
   545  //	bar1, b2 some other string here
   546  //
   547  // We want to offset the 2nd row usage by some amount so that everything
   548  // is aligned
   549  //
   550  //	foo1, foo2, foo3  some string here
   551  //	bar1, b2          some other string here
   552  //
   553  // to find that offset we find the length of all the rows and use the max
   554  // to calculate the offset
   555  func offsetCommands(cmds []*Command, fixed int) int {
   556  	var max int = 0
   557  	for _, cmd := range cmds {
   558  		s := strings.Join(cmd.Names(), ", ")
   559  		if len(s) > max {
   560  			max = len(s)
   561  		}
   562  	}
   563  	return max + fixed
   564  }
   565  

View as plain text