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