tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/engine/cli/help.go (about)

     1  package cli
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"io"
     7  	"reflect"
     8  	"strings"
     9  	"text/template"
    10  	"unicode"
    11  )
    12  
    13  // HelpFuncs are used by the help templating system.
    14  var HelpFuncs = template.FuncMap{
    15  	"trim": strings.TrimSpace,
    16  	"trimRight": func(s string) string {
    17  		return strings.TrimRightFunc(s, unicode.IsSpace)
    18  	},
    19  	"padRight": func(s string, padding int) string {
    20  		template := fmt.Sprintf("%%-%ds", padding)
    21  		return fmt.Sprintf(template, s)
    22  	},
    23  }
    24  
    25  // HelpTemplate is a template used to generate help.
    26  var HelpTemplate = `Usage:{{if .Runnable}}
    27  {{.UseLine}}{{end}}{{if .HasSubCommands}}
    28  {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
    29  
    30  Aliases:
    31  {{.NameAndAliases}}{{end}}{{if .HasExample}}
    32  
    33  Examples:
    34  {{.Example}}{{end}}{{if .HasSubCommands}}
    35  
    36  Available Commands:{{range .Commands}}{{if (or .Available (eq .Name "help"))}}
    37  {{padRight .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasFlags}}
    38  
    39  Flags:
    40  {{.FlagUsages | trimRight }}{{end}}{{if .HasSubCommands}}
    41  
    42  Use "{{.CommandPath}} [command] -help" for more information about a command.{{end}}
    43  `
    44  
    45  // CommandHelp wraps a Command to generate help.
    46  type CommandHelp struct {
    47  	*Command
    48  }
    49  
    50  // WriteHelp generates help for the command written to an io.Writer.
    51  func (c *CommandHelp) WriteHelp(w io.Writer) error {
    52  	t := template.Must(template.New("help").Funcs(HelpFuncs).Parse(HelpTemplate))
    53  	return t.Execute(w, c)
    54  }
    55  
    56  // Runnable determines if the command is itself runnable.
    57  func (c *CommandHelp) Runnable() bool {
    58  	return c.Run != nil
    59  }
    60  
    61  // Available determines if a command is available as a non-help command (this includes all non hidden commands).
    62  func (c *CommandHelp) Available() bool {
    63  	if c.Hidden {
    64  		return false
    65  	}
    66  	if c.Runnable() || c.HasSubCommands() {
    67  		return true
    68  	}
    69  	return false
    70  }
    71  
    72  // HasSubCommands determines if a command has available sub commands that need to be
    73  // shown in the usage/help default template under 'available commands'.
    74  func (c *CommandHelp) HasSubCommands() bool {
    75  	for _, sub := range c.commands {
    76  		if (&CommandHelp{sub}).Available() {
    77  			return true
    78  		}
    79  	}
    80  	return false
    81  }
    82  
    83  // NameAndAliases returns a list of the command name and all aliases.
    84  func (c *CommandHelp) NameAndAliases() string {
    85  	return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ")
    86  }
    87  
    88  // HasExample determines if the command has example.
    89  func (c *CommandHelp) HasExample() bool {
    90  	return len(c.Example) > 0
    91  }
    92  
    93  // Commands returns any subcommands as CommandHelp values.
    94  func (c *CommandHelp) Commands() (cmds []*CommandHelp) {
    95  	for _, cmd := range c.commands {
    96  		cmds = append(cmds, &CommandHelp{cmd})
    97  	}
    98  	return
    99  }
   100  
   101  // NamePadding returns padding for the name.
   102  func (c *CommandHelp) NamePadding() int {
   103  	// TODO: consider making this dynamic, based on length of all sibling commands
   104  	return 16
   105  }
   106  
   107  // HasFlags checks if the command contains flags.
   108  func (c *CommandHelp) HasFlags() bool {
   109  	n := 0
   110  	c.Flags().VisitAll(func(f *flag.Flag) {
   111  		n++
   112  	})
   113  	return n > 0
   114  }
   115  
   116  // FlagUsages creates a string for flag usage help.
   117  func (c *CommandHelp) FlagUsages() string {
   118  	var sb strings.Builder
   119  	c.Flags().VisitAll(func(f *flag.Flag) {
   120  		// Two spaces before - or --; see next two comments.
   121  		// Use - for single letter flags, -- for longer
   122  		if len(f.Name) == 1 {
   123  			fmt.Fprintf(&sb, "  -%s", f.Name)
   124  		} else {
   125  			fmt.Fprintf(&sb, "  --%s", f.Name)
   126  		}
   127  
   128  		name, usage := flag.UnquoteUsage(f)
   129  		if len(name) > 0 {
   130  			sb.WriteString(" ")
   131  			sb.WriteString(name)
   132  		}
   133  		// Boolean flags of one ASCII letter are so common we
   134  		// treat them specially, putting their usage on the same line.
   135  		if sb.Len() <= 4 { // space, space, '-', 'x'.
   136  			sb.WriteString("\t")
   137  		} else {
   138  			// Four spaces before the tab triggers good alignment
   139  			// for both 4- and 8-space tab stops.
   140  			sb.WriteString("\n    \t")
   141  		}
   142  		sb.WriteString(strings.ReplaceAll(usage, "\n", "\n    \t"))
   143  		f.Usage = ""
   144  		if !isZeroValue(f, f.DefValue) {
   145  			typ, _ := flag.UnquoteUsage(f)
   146  			if typ == "string" {
   147  				// put quotes on the value
   148  				fmt.Fprintf(&sb, " (default %q)", f.DefValue)
   149  			} else {
   150  				fmt.Fprintf(&sb, " (default %v)", f.DefValue)
   151  			}
   152  		}
   153  		sb.WriteString("\n")
   154  	})
   155  	return sb.String()
   156  }
   157  
   158  // isZeroValue determines whether the string represents the zero
   159  // value for a flag.
   160  func isZeroValue(f *flag.Flag, value string) bool {
   161  	// Build a zero value of the flag's Value type, and see if the
   162  	// result of calling its String method equals the value passed in.
   163  	// This works unless the Value type is itself an interface type.
   164  	typ := reflect.TypeOf(f.Value)
   165  	var z reflect.Value
   166  	if typ.Kind() == reflect.Ptr {
   167  		z = reflect.New(typ.Elem())
   168  	} else {
   169  		z = reflect.Zero(typ)
   170  	}
   171  	return value == z.Interface().(flag.Value).String()
   172  }