github.com/maresnic/mr-kong@v1.0.0/help.go (about)

     1  package kong
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/doc"
     7  	"io"
     8  	"strings"
     9  )
    10  
    11  const (
    12  	defaultIndent        = 2
    13  	defaultColumnPadding = 4
    14  )
    15  
    16  // Help flag.
    17  type helpValue bool
    18  
    19  func (h helpValue) BeforeReset(ctx *Context) error {
    20  	options := ctx.Kong.helpOptions
    21  	options.Summary = false
    22  	err := ctx.Kong.help(options, ctx)
    23  	if err != nil {
    24  		return err
    25  	}
    26  	ctx.Kong.Exit(0)
    27  	return nil
    28  }
    29  
    30  // HelpOptions for HelpPrinters.
    31  type HelpOptions struct {
    32  	// Don't print top-level usage summary.
    33  	NoAppSummary bool
    34  
    35  	// Write a one-line summary of the context.
    36  	Summary bool
    37  
    38  	// Write help in a more compact, but still fully-specified, form.
    39  	Compact bool
    40  
    41  	// Tree writes command chains in a tree structure instead of listing them separately.
    42  	Tree bool
    43  
    44  	// Place the flags after the commands listing.
    45  	FlagsLast bool
    46  
    47  	// Indenter modulates the given prefix for the next layer in the tree view.
    48  	// The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter
    49  	// The kong.SpaceIndenter will be used by default.
    50  	Indenter HelpIndenter
    51  
    52  	// Don't show the help associated with subcommands
    53  	NoExpandSubcommands bool
    54  
    55  	// Clamp the help wrap width to a value smaller than the terminal width.
    56  	// If this is set to a non-positive number, the terminal width is used; otherwise,
    57  	// the min of this value or the terminal width is used.
    58  	WrapUpperBound int
    59  }
    60  
    61  // Apply options to Kong as a configuration option.
    62  func (h HelpOptions) Apply(k *Kong) error {
    63  	k.helpOptions = h
    64  	return nil
    65  }
    66  
    67  // HelpProvider can be implemented by commands/args to provide detailed help.
    68  type HelpProvider interface {
    69  	// This string is formatted by go/doc and thus has the same formatting rules.
    70  	Help() string
    71  }
    72  
    73  // PlaceHolderProvider can be implemented by mappers to provide custom placeholder text.
    74  type PlaceHolderProvider interface {
    75  	PlaceHolder(flag *Flag) string
    76  }
    77  
    78  // HelpIndenter is used to indent new layers in the help tree.
    79  type HelpIndenter func(prefix string) string
    80  
    81  // HelpPrinter is used to print context-sensitive help.
    82  type HelpPrinter func(options HelpOptions, ctx *Context) error
    83  
    84  // HelpValueFormatter is used to format the help text of flags and positional arguments.
    85  type HelpValueFormatter func(value *Value) string
    86  
    87  // DefaultHelpValueFormatter is the default HelpValueFormatter.
    88  func DefaultHelpValueFormatter(value *Value) string {
    89  	if len(value.Tag.Envs) == 0 || HasInterpolatedVar(value.OrigHelp, "env") {
    90  		return value.Help
    91  	}
    92  	suffix := "(" + formatEnvs(value.Tag.Envs) + ")"
    93  	switch {
    94  	case strings.HasSuffix(value.Help, "."):
    95  		return value.Help[:len(value.Help)-1] + " " + suffix + "."
    96  	case value.Help == "":
    97  		return suffix
    98  	default:
    99  		return value.Help + " " + suffix
   100  	}
   101  }
   102  
   103  // DefaultShortHelpPrinter is the default HelpPrinter for short help on error.
   104  func DefaultShortHelpPrinter(options HelpOptions, ctx *Context) error {
   105  	w := newHelpWriter(ctx, options)
   106  	cmd := ctx.Selected()
   107  	app := ctx.Model
   108  	if cmd == nil {
   109  		w.Printf("Usage: %s%s", app.Name, app.Summary())
   110  		w.Printf(`Run "%s --help" for more information.`, app.Name)
   111  	} else {
   112  		w.Printf("Usage: %s %s", app.Name, cmd.Summary())
   113  		w.Printf(`Run "%s --help" for more information.`, cmd.FullPath())
   114  	}
   115  	return w.Write(ctx.Stdout)
   116  }
   117  
   118  // DefaultHelpPrinter is the default HelpPrinter.
   119  func DefaultHelpPrinter(options HelpOptions, ctx *Context) error {
   120  	if ctx.Empty() {
   121  		options.Summary = false
   122  	}
   123  	w := newHelpWriter(ctx, options)
   124  	selected := ctx.Selected()
   125  	if selected == nil {
   126  		printApp(w, ctx.Model)
   127  	} else {
   128  		printCommand(w, ctx.Model, selected)
   129  	}
   130  	return w.Write(ctx.Stdout)
   131  }
   132  
   133  func printApp(w *helpWriter, app *Application) {
   134  	if !w.NoAppSummary {
   135  		w.Printf("Usage: %s%s", app.Name, app.Summary())
   136  	}
   137  	printNodeDetail(w, app.Node, true)
   138  	cmds := app.Leaves(true)
   139  	if len(cmds) > 0 && app.HelpFlag != nil {
   140  		w.Print("")
   141  		if w.Summary {
   142  			w.Printf(`Run "%s --help" for more information.`, app.Name)
   143  		} else {
   144  			w.Printf(`Run "%s <command> --help" for more information on a command.`, app.Name)
   145  		}
   146  	}
   147  }
   148  
   149  func printCommand(w *helpWriter, app *Application, cmd *Command) {
   150  	if !w.NoAppSummary {
   151  		w.Printf("Usage: %s %s", app.Name, cmd.Summary())
   152  	}
   153  	printNodeDetail(w, cmd, true)
   154  	if w.Summary && app.HelpFlag != nil {
   155  		w.Print("")
   156  		w.Printf(`Run "%s --help" for more information.`, cmd.FullPath())
   157  	}
   158  }
   159  
   160  func printNodeDetail(w *helpWriter, node *Node, hide bool) {
   161  	if node.Help != "" {
   162  		w.Print("")
   163  		w.Wrap(node.Help)
   164  	}
   165  	if w.Summary {
   166  		return
   167  	}
   168  	if node.Detail != "" {
   169  		w.Print("")
   170  		w.Wrap(node.Detail)
   171  	}
   172  	if len(node.Positional) > 0 {
   173  		w.Print("")
   174  		w.Print("Arguments:")
   175  		writePositionals(w.Indent(), node.Positional)
   176  	}
   177  	printFlags := func() {
   178  		if flags := node.AllFlags(true); len(flags) > 0 {
   179  			groupedFlags := collectFlagGroups(flags)
   180  			for _, group := range groupedFlags {
   181  				w.Print("")
   182  				if group.Metadata.Title != "" {
   183  					w.Wrap(group.Metadata.Title)
   184  				}
   185  				if group.Metadata.Description != "" {
   186  					w.Indent().Wrap(group.Metadata.Description)
   187  					w.Print("")
   188  				}
   189  				writeFlags(w.Indent(), group.Flags)
   190  			}
   191  		}
   192  	}
   193  	if !w.FlagsLast {
   194  		printFlags()
   195  	}
   196  	var cmds []*Node
   197  	if w.NoExpandSubcommands {
   198  		cmds = node.Children
   199  	} else {
   200  		cmds = node.Leaves(hide)
   201  	}
   202  	if len(cmds) > 0 {
   203  		iw := w.Indent()
   204  		if w.Tree {
   205  			w.Print("")
   206  			w.Print("Commands:")
   207  			writeCommandTree(iw, node)
   208  		} else {
   209  			groupedCmds := collectCommandGroups(cmds)
   210  			for _, group := range groupedCmds {
   211  				w.Print("")
   212  				if group.Metadata.Title != "" {
   213  					w.Wrap(group.Metadata.Title)
   214  				}
   215  				if group.Metadata.Description != "" {
   216  					w.Indent().Wrap(group.Metadata.Description)
   217  					w.Print("")
   218  				}
   219  
   220  				if w.Compact {
   221  					writeCompactCommandList(group.Commands, iw)
   222  				} else {
   223  					writeCommandList(group.Commands, iw)
   224  				}
   225  			}
   226  		}
   227  	}
   228  	if w.FlagsLast {
   229  		printFlags()
   230  	}
   231  }
   232  
   233  func writeCommandList(cmds []*Node, iw *helpWriter) {
   234  	for i, cmd := range cmds {
   235  		if cmd.Hidden {
   236  			continue
   237  		}
   238  		printCommandSummary(iw, cmd)
   239  		if i != len(cmds)-1 {
   240  			iw.Print("")
   241  		}
   242  	}
   243  }
   244  
   245  func writeCompactCommandList(cmds []*Node, iw *helpWriter) {
   246  	rows := [][2]string{}
   247  	for _, cmd := range cmds {
   248  		if cmd.Hidden {
   249  			continue
   250  		}
   251  		rows = append(rows, [2]string{cmd.Path(), cmd.Help})
   252  	}
   253  	writeTwoColumns(iw, rows)
   254  }
   255  
   256  func writeCommandTree(w *helpWriter, node *Node) {
   257  	rows := make([][2]string, 0, len(node.Children)*2)
   258  	for i, cmd := range node.Children {
   259  		if cmd.Hidden {
   260  			continue
   261  		}
   262  		rows = append(rows, w.CommandTree(cmd, "")...)
   263  		if i != len(node.Children)-1 {
   264  			rows = append(rows, [2]string{"", ""})
   265  		}
   266  	}
   267  	writeTwoColumns(w, rows)
   268  }
   269  
   270  type helpFlagGroup struct {
   271  	Metadata *Group
   272  	Flags    [][]*Flag
   273  }
   274  
   275  func collectFlagGroups(flags [][]*Flag) []helpFlagGroup {
   276  	// Group keys in order of appearance.
   277  	groups := []*Group{}
   278  	// Flags grouped by their group key.
   279  	flagsByGroup := map[string][][]*Flag{}
   280  
   281  	for _, levelFlags := range flags {
   282  		levelFlagsByGroup := map[string][]*Flag{}
   283  
   284  		for _, flag := range levelFlags {
   285  			key := ""
   286  			if flag.Group != nil {
   287  				key = flag.Group.Key
   288  				groupAlreadySeen := false
   289  				for _, group := range groups {
   290  					if key == group.Key {
   291  						groupAlreadySeen = true
   292  						break
   293  					}
   294  				}
   295  				if !groupAlreadySeen {
   296  					groups = append(groups, flag.Group)
   297  				}
   298  			}
   299  
   300  			levelFlagsByGroup[key] = append(levelFlagsByGroup[key], flag)
   301  		}
   302  
   303  		for key, flags := range levelFlagsByGroup {
   304  			flagsByGroup[key] = append(flagsByGroup[key], flags)
   305  		}
   306  	}
   307  
   308  	out := []helpFlagGroup{}
   309  	// Ungrouped flags are always displayed first.
   310  	if ungroupedFlags, ok := flagsByGroup[""]; ok {
   311  		out = append(out, helpFlagGroup{
   312  			Metadata: &Group{Title: "Flags:"},
   313  			Flags:    ungroupedFlags,
   314  		})
   315  	}
   316  	for _, group := range groups {
   317  		out = append(out, helpFlagGroup{Metadata: group, Flags: flagsByGroup[group.Key]})
   318  	}
   319  	return out
   320  }
   321  
   322  type helpCommandGroup struct {
   323  	Metadata *Group
   324  	Commands []*Node
   325  }
   326  
   327  func collectCommandGroups(nodes []*Node) []helpCommandGroup {
   328  	// Groups in order of appearance.
   329  	groups := []*Group{}
   330  	// Nodes grouped by their group key.
   331  	nodesByGroup := map[string][]*Node{}
   332  
   333  	for _, node := range nodes {
   334  		key := ""
   335  		if group := node.ClosestGroup(); group != nil {
   336  			key = group.Key
   337  			if _, ok := nodesByGroup[key]; !ok {
   338  				groups = append(groups, group)
   339  			}
   340  		}
   341  		nodesByGroup[key] = append(nodesByGroup[key], node)
   342  	}
   343  
   344  	out := []helpCommandGroup{}
   345  	// Ungrouped nodes are always displayed first.
   346  	if ungroupedNodes, ok := nodesByGroup[""]; ok {
   347  		out = append(out, helpCommandGroup{
   348  			Metadata: &Group{Title: "Commands:"},
   349  			Commands: ungroupedNodes,
   350  		})
   351  	}
   352  	for _, group := range groups {
   353  		out = append(out, helpCommandGroup{Metadata: group, Commands: nodesByGroup[group.Key]})
   354  	}
   355  	return out
   356  }
   357  
   358  func printCommandSummary(w *helpWriter, cmd *Command) {
   359  	w.Print(cmd.Summary())
   360  	if cmd.Help != "" {
   361  		w.Indent().Wrap(cmd.Help)
   362  	}
   363  }
   364  
   365  type helpWriter struct {
   366  	indent        string
   367  	width         int
   368  	lines         *[]string
   369  	helpFormatter HelpValueFormatter
   370  	HelpOptions
   371  }
   372  
   373  func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter {
   374  	lines := []string{}
   375  	wrapWidth := guessWidth(ctx.Stdout)
   376  	if options.WrapUpperBound > 0 && wrapWidth > options.WrapUpperBound {
   377  		wrapWidth = options.WrapUpperBound
   378  	}
   379  	w := &helpWriter{
   380  		indent:        "",
   381  		width:         wrapWidth,
   382  		lines:         &lines,
   383  		helpFormatter: ctx.Kong.helpFormatter,
   384  		HelpOptions:   options,
   385  	}
   386  	return w
   387  }
   388  
   389  func (h *helpWriter) Printf(format string, args ...interface{}) {
   390  	h.Print(fmt.Sprintf(format, args...))
   391  }
   392  
   393  func (h *helpWriter) Print(text string) {
   394  	*h.lines = append(*h.lines, strings.TrimRight(h.indent+text, " "))
   395  }
   396  
   397  // Indent returns a new helpWriter indented by two characters.
   398  func (h *helpWriter) Indent() *helpWriter {
   399  	return &helpWriter{indent: h.indent + "  ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions, helpFormatter: h.helpFormatter}
   400  }
   401  
   402  func (h *helpWriter) String() string {
   403  	return strings.Join(*h.lines, "\n")
   404  }
   405  
   406  func (h *helpWriter) Write(w io.Writer) error {
   407  	for _, line := range *h.lines {
   408  		_, err := io.WriteString(w, line+"\n")
   409  		if err != nil {
   410  			return err
   411  		}
   412  	}
   413  	return nil
   414  }
   415  
   416  func (h *helpWriter) Wrap(text string) {
   417  	w := bytes.NewBuffer(nil)
   418  	doc.ToText(w, strings.TrimSpace(text), "", "    ", h.width)
   419  	for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") {
   420  		h.Print(line)
   421  	}
   422  }
   423  
   424  func writePositionals(w *helpWriter, args []*Positional) {
   425  	rows := [][2]string{}
   426  	for _, arg := range args {
   427  		rows = append(rows, [2]string{arg.Summary(), w.helpFormatter(arg)})
   428  	}
   429  	writeTwoColumns(w, rows)
   430  }
   431  
   432  func writeFlags(w *helpWriter, groups [][]*Flag) {
   433  	rows := [][2]string{}
   434  	haveShort := false
   435  	for _, group := range groups {
   436  		for _, flag := range group {
   437  			if flag.Short != 0 {
   438  				haveShort = true
   439  				break
   440  			}
   441  		}
   442  	}
   443  	for i, group := range groups {
   444  		if i > 0 {
   445  			rows = append(rows, [2]string{"", ""})
   446  		}
   447  		for _, flag := range group {
   448  			if !flag.Hidden {
   449  				rows = append(rows, [2]string{formatFlag(haveShort, flag), w.helpFormatter(flag.Value)})
   450  			}
   451  		}
   452  	}
   453  	writeTwoColumns(w, rows)
   454  }
   455  
   456  func writeTwoColumns(w *helpWriter, rows [][2]string) {
   457  	maxLeft := 375 * w.width / 1000
   458  	if maxLeft < 30 {
   459  		maxLeft = 30
   460  	}
   461  	// Find size of first column.
   462  	leftSize := 0
   463  	for _, row := range rows {
   464  		if c := len(row[0]); c > leftSize && c < maxLeft {
   465  			leftSize = c
   466  		}
   467  	}
   468  
   469  	offsetStr := strings.Repeat(" ", leftSize+defaultColumnPadding)
   470  
   471  	for _, row := range rows {
   472  		buf := bytes.NewBuffer(nil)
   473  		doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-defaultColumnPadding)
   474  		lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
   475  
   476  		line := fmt.Sprintf("%-*s", leftSize, row[0])
   477  		if len(row[0]) < maxLeft {
   478  			line += fmt.Sprintf("%*s%s", defaultColumnPadding, "", lines[0])
   479  			lines = lines[1:]
   480  		}
   481  		w.Print(line)
   482  		for _, line := range lines {
   483  			w.Printf("%s%s", offsetStr, line)
   484  		}
   485  	}
   486  }
   487  
   488  // haveShort will be true if there are short flags present at all in the help. Useful for column alignment.
   489  func formatFlag(haveShort bool, flag *Flag) string {
   490  	flagString := ""
   491  	name := flag.Name
   492  	isBool := flag.IsBool()
   493  	isCounter := flag.IsCounter()
   494  	if flag.Short != 0 {
   495  		if isBool && flag.Tag.Negatable {
   496  			flagString += fmt.Sprintf("-%c, --[no-]%s", flag.Short, name)
   497  		} else {
   498  			flagString += fmt.Sprintf("-%c, --%s", flag.Short, name)
   499  		}
   500  	} else {
   501  		if isBool && flag.Tag.Negatable {
   502  			if haveShort {
   503  				flagString = fmt.Sprintf("    --[no-]%s", name)
   504  			} else {
   505  				flagString = fmt.Sprintf("--[no-]%s", name)
   506  			}
   507  		} else {
   508  			if haveShort {
   509  				flagString += fmt.Sprintf("    --%s", name)
   510  			} else {
   511  				flagString += fmt.Sprintf("--%s", name)
   512  			}
   513  		}
   514  	}
   515  	if !isBool && !isCounter {
   516  		flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder())
   517  	}
   518  	return flagString
   519  }
   520  
   521  // CommandTree creates a tree with the given node name as root and its children's arguments and sub commands as leaves.
   522  func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string) {
   523  	var nodeName string
   524  	switch node.Type {
   525  	default:
   526  		nodeName += prefix + node.Name
   527  		if len(node.Aliases) != 0 {
   528  			nodeName += fmt.Sprintf(" (%s)", strings.Join(node.Aliases, ","))
   529  		}
   530  	case ArgumentNode:
   531  		nodeName += prefix + "<" + node.Name + ">"
   532  	}
   533  	rows = append(rows, [2]string{nodeName, node.Help})
   534  	if h.Indenter == nil {
   535  		prefix = SpaceIndenter(prefix)
   536  	} else {
   537  		prefix = h.Indenter(prefix)
   538  	}
   539  	for _, arg := range node.Positional {
   540  		rows = append(rows, [2]string{prefix + arg.Summary(), arg.Help})
   541  	}
   542  	for _, subCmd := range node.Children {
   543  		if subCmd.Hidden {
   544  			continue
   545  		}
   546  		rows = append(rows, h.CommandTree(subCmd, prefix)...)
   547  	}
   548  	return
   549  }
   550  
   551  // SpaceIndenter adds a space indent to the given prefix.
   552  func SpaceIndenter(prefix string) string {
   553  	return prefix + strings.Repeat(" ", defaultIndent)
   554  }
   555  
   556  // LineIndenter adds line points to every new indent.
   557  func LineIndenter(prefix string) string {
   558  	if prefix == "" {
   559  		return "- "
   560  	}
   561  	return strings.Repeat(" ", defaultIndent) + prefix
   562  }
   563  
   564  // TreeIndenter adds line points to every new indent and vertical lines to every layer.
   565  func TreeIndenter(prefix string) string {
   566  	if prefix == "" {
   567  		return "|- "
   568  	}
   569  	return "|" + strings.Repeat(" ", defaultIndent) + prefix
   570  }
   571  
   572  func formatEnvs(envs []string) string {
   573  	formatted := make([]string, len(envs))
   574  	for i := range envs {
   575  		formatted[i] = "$" + envs[i]
   576  	}
   577  
   578  	return strings.Join(formatted, ", ")
   579  }