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 }