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 }