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 { 220 if option.DefaultMask != "-" { 221 def = option.DefaultMask 222 } 223 } else { 224 def = option.defaultLiteral 225 } 226 227 var envDef string 228 if option.EnvDefaultKey != "" { 229 var envPrintable string 230 if runtime.GOOS == "windows" { 231 envPrintable = "%" + option.EnvDefaultKey + "%" 232 } else { 233 envPrintable = "$" + option.EnvDefaultKey 234 } 235 envDef = fmt.Sprintf(" [%s]", envPrintable) 236 } 237 238 var desc string 239 240 if def != "" { 241 desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef) 242 } else { 243 desc = option.Description + envDef 244 } 245 246 writer.WriteString(wrapText(desc, 247 info.terminalColumns-descstart, 248 strings.Repeat(" ", descstart))) 249 } 250 251 writer.WriteString("\n") 252} 253 254func maxCommandLength(s []*Command) int { 255 if len(s) == 0 { 256 return 0 257 } 258 259 ret := len(s[0].Name) 260 261 for _, v := range s[1:] { 262 l := len(v.Name) 263 264 if l > ret { 265 ret = l 266 } 267 } 268 269 return ret 270} 271 272// WriteHelp writes a help message containing all the possible options and 273// their descriptions to the provided writer. Note that the HelpFlag parser 274// option provides a convenient way to add a -h/--help option group to the 275// command line parser which will automatically show the help messages using 276// this method. 277func (p *Parser) WriteHelp(writer io.Writer) { 278 if writer == nil { 279 return 280 } 281 282 wr := bufio.NewWriter(writer) 283 aligninfo := p.getAlignmentInfo() 284 285 cmd := p.Command 286 287 for cmd.Active != nil { 288 cmd = cmd.Active 289 } 290 291 if p.Name != "" { 292 wr.WriteString("Usage:\n") 293 wr.WriteString(" ") 294 295 allcmd := p.Command 296 297 for allcmd != nil { 298 var usage string 299 300 if allcmd == p.Command { 301 if len(p.Usage) != 0 { 302 usage = p.Usage 303 } else if p.Options&HelpFlag != 0 { 304 usage = "[OPTIONS]" 305 } 306 } else if us, ok := allcmd.data.(Usage); ok { 307 usage = us.Usage() 308 } else if allcmd.hasCliOptions() { 309 usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) 310 } 311 312 if len(usage) != 0 { 313 fmt.Fprintf(wr, " %s %s", allcmd.Name, usage) 314 } else { 315 fmt.Fprintf(wr, " %s", allcmd.Name) 316 } 317 318 if len(allcmd.args) > 0 { 319 fmt.Fprintf(wr, " ") 320 } 321 322 for i, arg := range allcmd.args { 323 if i != 0 { 324 fmt.Fprintf(wr, " ") 325 } 326 327 name := arg.Name 328 329 if arg.isRemaining() { 330 name = name + "..." 331 } 332 333 if !allcmd.ArgsRequired { 334 fmt.Fprintf(wr, "[%s]", name) 335 } else { 336 fmt.Fprintf(wr, "%s", name) 337 } 338 } 339 340 if allcmd.Active == nil && len(allcmd.commands) > 0 { 341 var co, cc string 342 343 if allcmd.SubcommandsOptional { 344 co, cc = "[", "]" 345 } else { 346 co, cc = "<", ">" 347 } 348 349 visibleCommands := allcmd.visibleCommands() 350 351 if len(visibleCommands) > 3 { 352 fmt.Fprintf(wr, " %scommand%s", co, cc) 353 } else { 354 subcommands := allcmd.sortedVisibleCommands() 355 names := make([]string, len(subcommands)) 356 357 for i, subc := range subcommands { 358 names[i] = subc.Name 359 } 360 361 fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc) 362 } 363 } 364 365 allcmd = allcmd.Active 366 } 367 368 fmt.Fprintln(wr) 369 370 if len(cmd.LongDescription) != 0 { 371 fmt.Fprintln(wr) 372 373 t := wrapText(cmd.LongDescription, 374 aligninfo.terminalColumns, 375 "") 376 377 fmt.Fprintln(wr, t) 378 } 379 } 380 381 c := p.Command 382 383 for c != nil { 384 printcmd := c != p.Command 385 386 c.eachGroup(func(grp *Group) { 387 first := true 388 389 // Skip built-in help group for all commands except the top-level 390 // parser 391 if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { 392 return 393 } 394 395 for _, info := range grp.options { 396 if !info.canCli() || info.Hidden { 397 continue 398 } 399 400 if printcmd { 401 fmt.Fprintf(wr, "\n[%s command options]\n", c.Name) 402 aligninfo.indent = true 403 printcmd = false 404 } 405 406 if first && cmd.Group != grp { 407 fmt.Fprintln(wr) 408 409 if aligninfo.indent { 410 wr.WriteString(" ") 411 } 412 413 fmt.Fprintf(wr, "%s:\n", grp.ShortDescription) 414 first = false 415 } 416 417 p.writeHelpOption(wr, info, aligninfo) 418 } 419 }) 420 421 var args []*Arg 422 for _, arg := range c.args { 423 if arg.Description != "" { 424 args = append(args, arg) 425 } 426 } 427 428 if len(args) > 0 { 429 if c == p.Command { 430 fmt.Fprintf(wr, "\nArguments:\n") 431 } else { 432 fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name) 433 } 434 435 descStart := aligninfo.descriptionStart() + paddingBeforeOption 436 437 for _, arg := range args { 438 argPrefix := strings.Repeat(" ", paddingBeforeOption) 439 argPrefix += arg.Name 440 441 if len(arg.Description) > 0 { 442 argPrefix += ":" 443 wr.WriteString(argPrefix) 444 445 // Space between "arg:" and the description start 446 descPadding := strings.Repeat(" ", descStart-len(argPrefix)) 447 // How much space the description gets before wrapping 448 descWidth := aligninfo.terminalColumns - 1 - descStart 449 // Whitespace to which we can indent new description lines 450 descPrefix := strings.Repeat(" ", descStart) 451 452 wr.WriteString(descPadding) 453 wr.WriteString(wrapText(arg.Description, descWidth, descPrefix)) 454 } else { 455 wr.WriteString(argPrefix) 456 } 457 458 fmt.Fprintln(wr) 459 } 460 } 461 462 c = c.Active 463 } 464 465 scommands := cmd.sortedVisibleCommands() 466 467 if len(scommands) > 0 { 468 maxnamelen := maxCommandLength(scommands) 469 470 fmt.Fprintln(wr) 471 fmt.Fprintln(wr, "Available commands:") 472 473 for _, c := range scommands { 474 fmt.Fprintf(wr, " %s", c.Name) 475 476 if len(c.ShortDescription) > 0 { 477 pad := strings.Repeat(" ", maxnamelen-len(c.Name)) 478 fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) 479 480 if len(c.Aliases) > 0 { 481 fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) 482 } 483 484 } 485 486 fmt.Fprintln(wr) 487 } 488 } 489 490 wr.Flush() 491} 492