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