1// Copyright 2012 Jesse van den Kieboom. 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 5package flags 6 7import ( 8 "bufio" 9 "bytes" 10 "fmt" 11 "io" 12 "runtime" 13 "strings" 14 "unicode/utf8" 15) 16 17type alignmentInfo struct { 18 maxLongLen int 19 hasShort bool 20 hasValueName bool 21 terminalColumns int 22 indent bool 23} 24 25const ( 26 paddingBeforeOption = 2 27 distanceBetweenOptionAndDescription = 2 28) 29 30func (a *alignmentInfo) descriptionStart() int { 31 ret := a.maxLongLen + distanceBetweenOptionAndDescription 32 33 if a.hasShort { 34 ret += 2 35 } 36 37 if a.maxLongLen > 0 { 38 ret += 4 39 } 40 41 if a.hasValueName { 42 ret += 3 43 } 44 45 return ret 46} 47 48func (a *alignmentInfo) updateLen(name string, indent bool) { 49 l := utf8.RuneCountInString(name) 50 51 if indent { 52 l = l + 4 53 } 54 55 if l > a.maxLongLen { 56 a.maxLongLen = l 57 } 58} 59 60func (p *Parser) getAlignmentInfo() alignmentInfo { 61 ret := alignmentInfo{ 62 maxLongLen: 0, 63 hasShort: false, 64 hasValueName: false, 65 terminalColumns: getTerminalColumns(), 66 } 67 68 if ret.terminalColumns <= 0 { 69 ret.terminalColumns = 80 70 } 71 72 var prevcmd *Command 73 74 p.eachActiveGroup(func(c *Command, grp *Group) { 75 if !grp.showInHelp() { 76 return 77 } 78 if c != prevcmd { 79 for _, arg := range c.args { 80 ret.updateLen(arg.Name, c != p.Command) 81 } 82 } 83 84 for _, info := range grp.options { 85 if !info.showInHelp() { 86 continue 87 } 88 89 if info.ShortName != 0 { 90 ret.hasShort = true 91 } 92 93 if len(info.ValueName) > 0 { 94 ret.hasValueName = true 95 } 96 97 l := info.LongNameWithNamespace() + info.ValueName 98 99 if len(info.Choices) != 0 { 100 l += "[" + strings.Join(info.Choices, "|") + "]" 101 } 102 103 ret.updateLen(l, c != p.Command) 104 } 105 }) 106 107 return ret 108} 109 110func wrapText(s string, l int, prefix string) string { 111 var ret string 112 113 if l < 10 { 114 l = 10 115 } 116 117 // Basic text wrapping of s at spaces to fit in l 118 lines := strings.Split(s, "\n") 119 120 for _, line := range lines { 121 var retline string 122 123 line = strings.TrimSpace(line) 124 125 for len(line) > l { 126 // Try to split on space 127 suffix := "" 128 129 pos := strings.LastIndex(line[:l], " ") 130 131 if pos < 0 { 132 pos = l - 1 133 suffix = "-\n" 134 } 135 136 if len(retline) != 0 { 137 retline += "\n" + prefix 138 } 139 140 retline += strings.TrimSpace(line[:pos]) + suffix 141 line = strings.TrimSpace(line[pos:]) 142 } 143 144 if len(line) > 0 { 145 if len(retline) != 0 { 146 retline += "\n" + prefix 147 } 148 149 retline += line 150 } 151 152 if len(ret) > 0 { 153 ret += "\n" 154 155 if len(retline) > 0 { 156 ret += prefix 157 } 158 } 159 160 ret += retline 161 } 162 163 return ret 164} 165 166func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { 167 line := &bytes.Buffer{} 168 169 prefix := paddingBeforeOption 170 171 if info.indent { 172 prefix += 4 173 } 174 175 if option.Hidden { 176 return 177 } 178 179 line.WriteString(strings.Repeat(" ", prefix)) 180 181 if option.ShortName != 0 { 182 line.WriteRune(defaultShortOptDelimiter) 183 line.WriteRune(option.ShortName) 184 } else if info.hasShort { 185 line.WriteString(" ") 186 } 187 188 descstart := info.descriptionStart() + paddingBeforeOption 189 190 if len(option.LongName) > 0 { 191 if option.ShortName != 0 { 192 line.WriteString(", ") 193 } else if info.hasShort { 194 line.WriteString(" ") 195 } 196 197 line.WriteString(defaultLongOptDelimiter) 198 line.WriteString(option.LongNameWithNamespace()) 199 } 200 201 if option.canArgument() { 202 line.WriteRune(defaultNameArgDelimiter) 203 204 if len(option.ValueName) > 0 { 205 line.WriteString(option.ValueName) 206 } 207 208 if len(option.Choices) > 0 { 209 line.WriteString("[" + strings.Join(option.Choices, "|") + "]") 210 } 211 } 212 213 written := line.Len() 214 line.WriteTo(writer) 215 216 if option.Description != "" { 217 dw := descstart - written 218 writer.WriteString(strings.Repeat(" ", dw)) 219 220 var def string 221 222 if len(option.DefaultMask) != 0 { 223 if option.DefaultMask != "-" { 224 def = option.DefaultMask 225 } 226 } else { 227 def = option.defaultLiteral 228 } 229 230 var envDef string 231 if option.EnvKeyWithNamespace() != "" { 232 var envPrintable string 233 if runtime.GOOS == "windows" { 234 envPrintable = "%" + option.EnvKeyWithNamespace() + "%" 235 } else { 236 envPrintable = "$" + option.EnvKeyWithNamespace() 237 } 238 envDef = fmt.Sprintf(" [%s]", envPrintable) 239 } 240 241 var desc string 242 243 if def != "" { 244 desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef) 245 } else { 246 desc = option.Description + envDef 247 } 248 249 writer.WriteString(wrapText(desc, 250 info.terminalColumns-descstart, 251 strings.Repeat(" ", descstart))) 252 } 253 254 writer.WriteString("\n") 255} 256 257func maxCommandLength(s []*Command) int { 258 if len(s) == 0 { 259 return 0 260 } 261 262 ret := len(s[0].Name) 263 264 for _, v := range s[1:] { 265 l := len(v.Name) 266 267 if l > ret { 268 ret = l 269 } 270 } 271 272 return ret 273} 274 275// WriteHelp writes a help message containing all the possible options and 276// their descriptions to the provided writer. Note that the HelpFlag parser 277// option provides a convenient way to add a -h/--help option group to the 278// command line parser which will automatically show the help messages using 279// this method. 280func (p *Parser) WriteHelp(writer io.Writer) { 281 if writer == nil { 282 return 283 } 284 285 wr := bufio.NewWriter(writer) 286 aligninfo := p.getAlignmentInfo() 287 288 cmd := p.Command 289 290 for cmd.Active != nil { 291 cmd = cmd.Active 292 } 293 294 if p.Name != "" { 295 wr.WriteString("Usage:\n") 296 wr.WriteString(" ") 297 298 allcmd := p.Command 299 300 for allcmd != nil { 301 var usage string 302 303 if allcmd == p.Command { 304 if len(p.Usage) != 0 { 305 usage = p.Usage 306 } else if p.Options&HelpFlag != 0 { 307 usage = "[OPTIONS]" 308 } 309 } else if us, ok := allcmd.data.(Usage); ok { 310 usage = us.Usage() 311 } else if allcmd.hasHelpOptions() { 312 usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) 313 } 314 315 if len(usage) != 0 { 316 fmt.Fprintf(wr, " %s %s", allcmd.Name, usage) 317 } else { 318 fmt.Fprintf(wr, " %s", allcmd.Name) 319 } 320 321 if len(allcmd.args) > 0 { 322 fmt.Fprintf(wr, " ") 323 } 324 325 for i, arg := range allcmd.args { 326 if i != 0 { 327 fmt.Fprintf(wr, " ") 328 } 329 330 name := arg.Name 331 332 if arg.isRemaining() { 333 name = name + "..." 334 } 335 336 if !allcmd.ArgsRequired { 337 fmt.Fprintf(wr, "[%s]", name) 338 } else { 339 fmt.Fprintf(wr, "%s", name) 340 } 341 } 342 343 if allcmd.Active == nil && len(allcmd.commands) > 0 { 344 var co, cc string 345 346 if allcmd.SubcommandsOptional { 347 co, cc = "[", "]" 348 } else { 349 co, cc = "<", ">" 350 } 351 352 visibleCommands := allcmd.visibleCommands() 353 354 if len(visibleCommands) > 3 { 355 fmt.Fprintf(wr, " %scommand%s", co, cc) 356 } else { 357 subcommands := allcmd.sortedVisibleCommands() 358 names := make([]string, len(subcommands)) 359 360 for i, subc := range subcommands { 361 names[i] = subc.Name 362 } 363 364 fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc) 365 } 366 } 367 368 allcmd = allcmd.Active 369 } 370 371 fmt.Fprintln(wr) 372 373 if len(cmd.LongDescription) != 0 { 374 fmt.Fprintln(wr) 375 376 t := wrapText(cmd.LongDescription, 377 aligninfo.terminalColumns, 378 "") 379 380 fmt.Fprintln(wr, t) 381 } 382 } 383 384 c := p.Command 385 386 for c != nil { 387 printcmd := c != p.Command 388 389 c.eachGroup(func(grp *Group) { 390 first := true 391 392 // Skip built-in help group for all commands except the top-level 393 // parser 394 if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { 395 return 396 } 397 398 for _, info := range grp.options { 399 if !info.showInHelp() { 400 continue 401 } 402 403 if printcmd { 404 fmt.Fprintf(wr, "\n[%s command options]\n", c.Name) 405 aligninfo.indent = true 406 printcmd = false 407 } 408 409 if first && cmd.Group != grp { 410 fmt.Fprintln(wr) 411 412 if aligninfo.indent { 413 wr.WriteString(" ") 414 } 415 416 fmt.Fprintf(wr, "%s:\n", grp.ShortDescription) 417 first = false 418 } 419 420 p.writeHelpOption(wr, info, aligninfo) 421 } 422 }) 423 424 var args []*Arg 425 for _, arg := range c.args { 426 if arg.Description != "" { 427 args = append(args, arg) 428 } 429 } 430 431 if len(args) > 0 { 432 if c == p.Command { 433 fmt.Fprintf(wr, "\nArguments:\n") 434 } else { 435 fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name) 436 } 437 438 descStart := aligninfo.descriptionStart() + paddingBeforeOption 439 440 for _, arg := range args { 441 argPrefix := strings.Repeat(" ", paddingBeforeOption) 442 argPrefix += arg.Name 443 444 if len(arg.Description) > 0 { 445 argPrefix += ":" 446 wr.WriteString(argPrefix) 447 448 // Space between "arg:" and the description start 449 descPadding := strings.Repeat(" ", descStart-len(argPrefix)) 450 // How much space the description gets before wrapping 451 descWidth := aligninfo.terminalColumns - 1 - descStart 452 // Whitespace to which we can indent new description lines 453 descPrefix := strings.Repeat(" ", descStart) 454 455 wr.WriteString(descPadding) 456 wr.WriteString(wrapText(arg.Description, descWidth, descPrefix)) 457 } else { 458 wr.WriteString(argPrefix) 459 } 460 461 fmt.Fprintln(wr) 462 } 463 } 464 465 c = c.Active 466 } 467 468 scommands := cmd.sortedVisibleCommands() 469 470 if len(scommands) > 0 { 471 maxnamelen := maxCommandLength(scommands) 472 473 fmt.Fprintln(wr) 474 fmt.Fprintln(wr, "Available commands:") 475 476 for _, c := range scommands { 477 fmt.Fprintf(wr, " %s", c.Name) 478 479 if len(c.ShortDescription) > 0 { 480 pad := strings.Repeat(" ", maxnamelen-len(c.Name)) 481 fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) 482 483 if len(c.Aliases) > 0 { 484 fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) 485 } 486 487 } 488 489 fmt.Fprintln(wr) 490 } 491 } 492 493 wr.Flush() 494} 495 496// WroteHelp is a helper to test the error from ParseArgs() to 497// determine if the help message was written. It is safe to 498// call without first checking that error is nil. 499func WroteHelp(err error) bool { 500 if err == nil { // No error 501 return false 502 } 503 504 flagError, ok := err.(*Error) 505 if !ok { // Not a go-flag error 506 return false 507 } 508 509 if flagError.Type != ErrHelp { // Did not print the help message 510 return false 511 } 512 513 return true 514} 515