github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cmdline/help.go (about) 1 // Copyright 2015 The Vanadium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cmdline 6 7 import ( 8 "bytes" 9 "flag" 10 "fmt" 11 "go/doc" 12 "io" 13 "path/filepath" 14 "regexp" 15 "strings" 16 "unicode" 17 "unicode/utf8" 18 19 "github.com/btwiuse/jiri/textutil" 20 ) 21 22 const missingDescription = "No description available" 23 24 // helpRunner is a Runner that implements the "help" functionality. Help is 25 // requested for the last command in path, which must not be empty. 26 type helpRunner struct { 27 path []*Command 28 *helpConfig 29 } 30 31 func makeHelpRunner(path []*Command, env *Env) helpRunner { 32 return helpRunner{path, &helpConfig{ 33 style: env.style(), 34 width: env.width(), 35 prefix: env.prefix(), 36 firstCall: env.firstCall(), 37 }} 38 } 39 40 // helpConfig holds configuration data for help. The style and width may be 41 // overriden by flags if the command returned by newCommand is parsed. 42 type helpConfig struct { 43 style style 44 width int 45 prefix string 46 firstCall bool 47 } 48 49 // Run implements the Runner interface method. 50 func (h helpRunner) Run(env *Env, args []string) error { 51 w := textutil.NewUTF8WrapWriter(env.Stdout, h.width) 52 defer w.Flush() 53 return runHelp(w, env, args, h.path, h.helpConfig) 54 } 55 56 // usageFunc is used as the implementation of the Env.Usage function. 57 func (h helpRunner) usageFunc(env *Env, writer io.Writer) { 58 w := textutil.NewUTF8WrapWriter(writer, h.width) 59 usage(w, env, h.path, h.helpConfig, h.helpConfig.firstCall) 60 w.Flush() 61 } 62 63 const ( 64 helpName = "help" 65 helpShort = "Display help for commands or topics" 66 ) 67 68 // newCommand returns a new help command that uses h as its Runner. 69 func (h helpRunner) newCommand() *Command { 70 help := &Command{ 71 Runner: h, 72 Name: helpName, 73 Short: helpShort, 74 Long: ` 75 Help with no args displays the usage of the parent command. 76 77 Help with args displays the usage of the specified sub-command or help topic. 78 79 "help ..." recursively displays help for all commands and topics. 80 `, 81 ArgsName: "[command/topic ...]", 82 ArgsLong: ` 83 [command/topic ...] optionally identifies a specific sub-command or help topic. 84 `, 85 } 86 help.Flags.Var(&h.style, "style", ` 87 The formatting style for help output: 88 compact - Good for compact cmdline output. 89 full - Good for cmdline output, shows all global flags. 90 godoc - Good for godoc processing. 91 shortonly - Only output short description. 92 Override the default by setting the CMDLINE_STYLE environment variable. 93 `) 94 help.Flags.IntVar(&h.width, "width", h.width, ` 95 Format output to this target width in runes, or unlimited if width < 0. 96 Defaults to the terminal width if available. Override the default by setting 97 the CMDLINE_WIDTH environment variable. 98 `) 99 // Override default values, so that the godoc style shows good defaults. 100 help.Flags.Lookup("style").DefValue = "compact" 101 help.Flags.Lookup("width").DefValue = "<terminal width>" 102 cleanTree(help) 103 return help 104 } 105 106 // runHelp implements the run-time behavior of the help command. 107 func runHelp(w *textutil.WrapWriter, env *Env, args []string, path []*Command, config *helpConfig) error { 108 if len(args) == 0 { 109 usage(w, env, path, config, config.firstCall) 110 return nil 111 } 112 if args[0] == "..." { 113 usageAll(w, env, path, config, config.firstCall) 114 return nil 115 } 116 // Look for matching children. 117 cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path) 118 subName, subArgs := args[0], args[1:] 119 for _, child := range cmd.Children { 120 if child.Name == subName { 121 return runHelp(w, env, subArgs, append(path, child), config) 122 } 123 } 124 if helpName == subName { 125 help := helpRunner{path, config}.newCommand() 126 return runHelp(w, env, subArgs, append(path, help), config) 127 } 128 if cmd.LookPath { 129 // Look for a matching executable in PATH. 130 if subCmd, _ := env.LookPath(cmd.Name + "-" + subName); subCmd != "" { 131 runner := binaryRunner{subCmd, cmdPath} 132 envCopy := env.clone() 133 envCopy.Vars["CMDLINE_STYLE"] = config.style.String() 134 if len(subArgs) == 0 { 135 return runner.Run(envCopy, []string{"-help"}) 136 } 137 return runner.Run(envCopy, append([]string{helpName}, subArgs...)) 138 } 139 } 140 // Look for matching topic. 141 for _, topic := range cmd.Topics { 142 if topic.Name == subName { 143 fmt.Fprintln(w, topic.Long) 144 return nil 145 } 146 } 147 fn := helpRunner{path, config}.usageFunc 148 return usageErrorf(env, fn, "%s: unknown command or topic %q", cmdPath, subName) 149 } 150 151 func godocHeader(path, short string) string { 152 // The first rune must be uppercase for godoc to recognize the string as a 153 // section header, which is linked to the table of contents. 154 switch { 155 case path == "": 156 return firstRuneToUpper(short) 157 case short == "": 158 return firstRuneToUpper(path) 159 } 160 // Godoc has special heurisitics to extract headers from the comments, from 161 // which it builds a nice table of contents. Headers must be single 162 // unindented lines with unindented paragraphs both before and after, and the 163 // line must not include certain characters. 164 // 165 // We try our best to create a header that includes both the command path and 166 // the short description, but if godoc won't extract a header out of the line, 167 // we fall back to just returning the command path. 168 // 169 // For more details see the comments and implementation of doc.ToHTML: 170 // http://golang.org/pkg/go/doc/#ToHTML 171 header := firstRuneToUpper(path + " - " + short) 172 var buf bytes.Buffer 173 doc.ToHTML(&buf, "before\n\n"+header+"\n\nafter", nil) 174 if !bytes.Contains(buf.Bytes(), []byte("<h")) { 175 return firstRuneToUpper(path) 176 } 177 return header 178 } 179 180 func firstRuneToUpper(s string) string { 181 if s == "" { 182 return "" 183 } 184 r, n := utf8.DecodeRuneInString(s) 185 return string(unicode.ToUpper(r)) + s[n:] 186 } 187 188 func lineBreak(w *textutil.WrapWriter, style style) { 189 w.Flush() 190 switch style { 191 case styleCompact, styleFull: 192 width := w.Width() 193 if width < 0 { 194 // If the user has chosen an "unlimited" word-wrapping width, we still 195 // need a reasonable width for our visual line break. 196 width = defaultWidth 197 } 198 fmt.Fprintln(w, strings.Repeat("=", width)) 199 case styleGoDoc: 200 fmt.Fprintln(w) 201 } 202 w.Flush() 203 } 204 205 // needsHelpChild returns true if cmd needs a default help command to be 206 // appended to its children. Every command that has children and doesn't 207 // already have a "help" command needs a help child. 208 func needsHelpChild(cmd *Command) bool { 209 for _, child := range cmd.Children { 210 if child.Name == helpName { 211 return false 212 } 213 } 214 return len(cmd.Children) > 0 215 } 216 217 // usageAll prints usage recursively via DFS from the path onward. 218 func usageAll(w *textutil.WrapWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) { 219 cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path) 220 usage(w, env, path, config, firstCall) 221 for _, child := range cmd.Children { 222 usageAll(w, env, append(path, child), config, false) 223 } 224 if firstCall && needsHelpChild(cmd) { 225 help := helpRunner{path, config}.newCommand() 226 usageAll(w, env, append(path, help), config, false) 227 } 228 if cmd.LookPath { 229 cmdPrefix := cmd.Name + "-" 230 subCmds, _ := env.LookPathPrefix(cmdPrefix, cmd.subNames(cmdPrefix)) 231 for _, subCmd := range subCmds { 232 runner := binaryRunner{subCmd, cmdPath} 233 var buffer bytes.Buffer 234 envCopy := env.clone() 235 envCopy.Stdout = &buffer 236 envCopy.Stderr = &buffer 237 envCopy.Vars["CMDLINE_FIRST_CALL"] = "false" 238 envCopy.Vars["CMDLINE_STYLE"] = config.style.String() 239 if err := runner.Run(envCopy, []string{helpName, "..."}); err == nil { 240 // The external child supports "help". 241 if config.style == styleGoDoc { 242 // The textutil package will discard any leading empty lines 243 // produced by the child process output, so we need to 244 // output it here. 245 fmt.Fprintln(w) 246 } 247 fmt.Fprint(w, buffer.String()) 248 continue 249 } 250 buffer.Reset() 251 if err := runner.Run(envCopy, []string{"-help"}); err == nil { 252 // The external child supports "-help". 253 if config.style == styleGoDoc { 254 // The textutil package will discard any leading empty lines 255 // produced by the child process output, so we need to 256 // output it here. 257 fmt.Fprintln(w) 258 } 259 fmt.Fprint(w, buffer.String()) 260 continue 261 } 262 // The external child does not support "help" or "-help". 263 lineBreak(w, config.style) 264 subName := strings.TrimPrefix(filepath.Base(subCmd), cmdPrefix) 265 fmt.Fprintln(w, godocHeader(cmdPath+" "+subName, missingDescription)) 266 } 267 } 268 for _, topic := range cmd.Topics { 269 lineBreak(w, config.style) 270 w.ForceVerbatim(true) 271 fmt.Fprintln(w, godocHeader(cmdPath+" "+topic.Name, topic.Short)) 272 w.ForceVerbatim(false) 273 fmt.Fprintln(w) 274 fmt.Fprintln(w, topic.Long) 275 } 276 } 277 278 // usage prints the usage of the last command in path to w. The bool firstCall 279 // is set to false when printing usage for multiple commands, and is used to 280 // avoid printing redundant information (e.g. help command, global flags). 281 func usage(w *textutil.WrapWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) { 282 cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path) 283 env.TimerPush("usage " + cmdPath) 284 defer env.TimerPop() 285 if config.style == styleShortOnly { 286 fmt.Fprintln(w, cmd.Short) 287 return 288 } 289 if !firstCall { 290 lineBreak(w, config.style) 291 w.ForceVerbatim(true) 292 fmt.Fprintln(w, godocHeader(cmdPath, cmd.Short)) 293 w.ForceVerbatim(false) 294 fmt.Fprintln(w) 295 } 296 fmt.Fprintln(w, cmd.Long) 297 fmt.Fprintln(w) 298 // Usage line. 299 fmt.Fprintln(w, "Usage:") 300 cmdPathF := " " + cmdPath 301 if countFlags(pathFlags(path), nil, true) > 0 || countFlags(globalFlags, nil, true) > 0 { 302 cmdPathF += " [flags]" 303 } 304 if cmd.Runner != nil { 305 if cmd.ArgsName != "" { 306 fmt.Fprintln(w, cmdPathF, cmd.ArgsName) 307 } else { 308 fmt.Fprintln(w, cmdPathF) 309 } 310 } 311 var extChildren []string 312 cmdPrefix := cmd.Name + "-" 313 if cmd.LookPath { 314 extChildren, _ = env.LookPathPrefix(cmdPrefix, cmd.subNames(cmdPrefix)) 315 } 316 hasSubcommands := len(cmd.Children) > 0 || len(extChildren) > 0 317 if hasSubcommands { 318 fmt.Fprintln(w, cmdPathF, "<command>") 319 fmt.Fprintln(w) 320 } 321 printShort := func(width int, name, short string) { 322 fmt.Fprintf(w, "%-[1]*[2]s %[3]s", width, name, short) 323 w.Flush() 324 } 325 const minNameWidth = 11 326 nameWidth := minNameWidth 327 for _, child := range cmd.Children { 328 if w := len(child.Name); w > nameWidth { 329 nameWidth = w 330 } 331 } 332 for _, extCmd := range extChildren { 333 extName := strings.TrimPrefix(filepath.Base(extCmd), cmdPrefix) 334 if w := len(extName); w > nameWidth { 335 nameWidth = w 336 } 337 } 338 // Built-in commands. 339 if len(cmd.Children) > 0 { 340 w.SetIndents() 341 fmt.Fprintln(w, "The", cmdPath, "commands are:") 342 // Print as a table with aligned columns Name and Short. 343 w.SetIndents(spaces(3), spaces(3+nameWidth+1)) 344 for _, child := range cmd.Children { 345 printShort(nameWidth, child.Name, child.Short) 346 } 347 // Default help command. 348 if firstCall && needsHelpChild(cmd) { 349 printShort(nameWidth, helpName, helpShort) 350 } 351 } 352 // External commands. 353 if len(extChildren) > 0 { 354 w.SetIndents() 355 fmt.Fprintln(w, "The", cmdPath, "external commands are:") 356 // Print as a table with aligned columns Name and Short. 357 w.SetIndents(spaces(3), spaces(3+nameWidth+1)) 358 for _, extCmd := range extChildren { 359 runner := binaryRunner{extCmd, cmdPath} 360 var buffer bytes.Buffer 361 envCopy := env.clone() 362 envCopy.Stdout = &buffer 363 envCopy.Stderr = &buffer 364 envCopy.Vars["CMDLINE_STYLE"] = "shortonly" 365 short := missingDescription 366 if err := runner.Run(envCopy, []string{"-help"}); err == nil { 367 // The external child supports "-help". 368 short = buffer.String() 369 } 370 extName := strings.TrimPrefix(filepath.Base(extCmd), cmdPrefix) 371 printShort(nameWidth, extName, short) 372 } 373 } 374 // Command footer. 375 if hasSubcommands { 376 w.SetIndents() 377 if firstCall && config.style != styleGoDoc { 378 fmt.Fprintf(w, "Run \"%s help [command]\" for command usage.\n", cmdPath) 379 } 380 } 381 // Args. 382 if cmd.Runner != nil && cmd.ArgsLong != "" { 383 fmt.Fprintln(w) 384 fmt.Fprintln(w, cmd.ArgsLong) 385 } 386 // Help topics. 387 if len(cmd.Topics) > 0 { 388 fmt.Fprintln(w) 389 fmt.Fprintln(w, "The", cmdPath, "additional help topics are:") 390 nameWidth := minNameWidth 391 for _, topic := range cmd.Topics { 392 if w := len(topic.Name); w > nameWidth { 393 nameWidth = w 394 } 395 } 396 // Print as a table with aligned columns Name and Short. 397 w.SetIndents(spaces(3), spaces(3+nameWidth+1)) 398 for _, topic := range cmd.Topics { 399 printShort(nameWidth, topic.Name, topic.Short) 400 } 401 w.SetIndents() 402 if firstCall && config.style != styleGoDoc { 403 fmt.Fprintf(w, "Run \"%s help [topic]\" for topic details.\n", cmdPath) 404 } 405 } 406 hidden := flagsUsage(w, path, config) 407 // Only show global flags on the first call. 408 if firstCall { 409 hidden = globalFlagsUsage(w, config) || hidden 410 } 411 if hidden { 412 fmt.Fprintln(w) 413 fullhelp := fmt.Sprintf(`Run "%s help -style=full" to show all flags.`, cmdPath) 414 if len(cmd.Children) == 0 { 415 if len(path) > 1 { 416 parentPath := pathName(config.prefix, path[:len(path)-1]) 417 fullhelp = fmt.Sprintf(`Run "%s help -style=full %s" to show all flags.`, parentPath, cmd.Name) 418 } else { 419 fullhelp = fmt.Sprintf(`Run "CMDLINE_STYLE=full %s -help" to show all flags.`, cmdPath) 420 } 421 } 422 fmt.Fprintln(w, fullhelp) 423 } 424 } 425 426 func flagsUsage(w *textutil.WrapWriter, path []*Command, config *helpConfig) bool { 427 cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path) 428 allFlags := pathFlags(path) 429 numCompact := countFlags(&cmd.Flags, nil, true) 430 numFull := countFlags(allFlags, nil, true) - numCompact 431 if config.style == styleCompact { 432 // Compact style, only show compact flags. 433 if numCompact > 0 { 434 fmt.Fprintln(w) 435 fmt.Fprintln(w, "The", cmdPath, "flags are:") 436 printFlags(w, &cmd.Flags, nil, config.style, nil, true) 437 } 438 return numFull > 0 439 } 440 // Non-compact style, always show all flags. 441 if numCompact > 0 || numFull > 0 { 442 fmt.Fprintln(w) 443 fmt.Fprintln(w, "The", cmdPath, "flags are:") 444 printFlags(w, &cmd.Flags, nil, config.style, nil, true) 445 if numCompact > 0 && numFull > 0 { 446 fmt.Fprintln(w) 447 } 448 printFlags(w, allFlags, &cmd.Flags, config.style, nil, true) 449 } 450 return false 451 } 452 453 func globalFlagsUsage(w *textutil.WrapWriter, config *helpConfig) bool { 454 regex := regexp.MustCompilePOSIX("^test\\..*$") 455 HideGlobalFlags(regex) 456 numCompact := countFlags(globalFlags, hiddenGlobalFlags, false) 457 numFull := countFlags(globalFlags, hiddenGlobalFlags, true) 458 if config.style == styleCompact { 459 // Compact style, only show compact flags. 460 if numCompact > 0 { 461 fmt.Fprintln(w) 462 fmt.Fprintln(w, "The global flags are:") 463 printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, false) 464 } 465 return numFull > 0 466 } 467 // Non-compact style, always show all global flags. 468 if numCompact > 0 || numFull > 0 { 469 fmt.Fprintln(w) 470 fmt.Fprintln(w, "The global flags are:") 471 printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, false) 472 if numCompact > 0 && numFull > 0 { 473 fmt.Fprintln(w) 474 } 475 printFlags(w, globalFlags, nil, config.style, hiddenGlobalFlags, true) 476 } 477 return false 478 } 479 480 func countFlags(flags *flag.FlagSet, regexps []*regexp.Regexp, match bool) (num int) { 481 flags.VisitAll(func(f *flag.Flag) { 482 if match == matchRegexps(regexps, f.Name) { 483 num++ 484 } 485 }) 486 return 487 } 488 489 func printFlags(w *textutil.WrapWriter, flags, filter *flag.FlagSet, style style, regexps []*regexp.Regexp, match bool) { 490 flags.VisitAll(func(f *flag.Flag) { 491 if filter != nil && filter.Lookup(f.Name) != nil { 492 return 493 } 494 if match != matchRegexps(regexps, f.Name) { 495 return 496 } 497 value := f.Value.String() 498 if style == styleGoDoc { 499 // When using styleGoDoc we use the default value, so that e.g. regular 500 // help will show "/usr/home/me/foo" while godoc will show "$HOME/foo". 501 value = f.DefValue 502 } 503 fmt.Fprintf(w, " -%s=%v", f.Name, value) 504 w.SetIndents(spaces(3)) 505 fmt.Fprintln(w, f.Usage) 506 w.SetIndents() 507 }) 508 } 509 510 func spaces(count int) string { 511 return strings.Repeat(" ", count) 512 } 513 514 func matchRegexps(regexps []*regexp.Regexp, name string) bool { 515 // We distinguish nil regexps from empty regexps; the former means "all names 516 // match", while the latter means "no names match". 517 if regexps == nil { 518 return true 519 } 520 for _, r := range regexps { 521 if r.MatchString(name) { 522 return true 523 } 524 } 525 return false 526 } 527 528 var hiddenGlobalFlags []*regexp.Regexp 529 530 // HideGlobalFlags hides global flags from the default compact-style usage 531 // message. Global flag names that match any of the regexps will not be shown 532 // in the compact usage message. Multiple calls behave as if all regexps were 533 // provided in a single call. 534 // 535 // All global flags are always shown in non-compact style usage messages. 536 func HideGlobalFlags(regexps ...*regexp.Regexp) { 537 hiddenGlobalFlags = append(hiddenGlobalFlags, regexps...) 538 if hiddenGlobalFlags == nil { 539 hiddenGlobalFlags = []*regexp.Regexp{} 540 } 541 }