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 "reflect" 13 "runtime" 14 "strings" 15 "unicode/utf8" 16) 17 18type alignmentInfo struct { 19 maxLongLen int 20 hasShort bool 21 hasValueName bool 22 terminalColumns int 23 indent bool 24} 25 26const ( 27 paddingBeforeOption = 2 28 distanceBetweenOptionAndDescription = 2 29) 30 31func (a *alignmentInfo) descriptionStart() int { 32 ret := a.maxLongLen + distanceBetweenOptionAndDescription 33 34 if a.hasShort { 35 ret += 2 36 } 37 38 if a.maxLongLen > 0 { 39 ret += 4 40 } 41 42 if a.hasValueName { 43 ret += 3 44 } 45 46 return ret 47} 48 49func (a *alignmentInfo) updateLen(name string, indent bool) { 50 l := utf8.RuneCountInString(name) 51 52 if indent { 53 l = l + 4 54 } 55 56 if l > a.maxLongLen { 57 a.maxLongLen = l 58 } 59} 60 61func (p *Parser) getAlignmentInfo() alignmentInfo { 62 ret := alignmentInfo{ 63 maxLongLen: 0, 64 hasShort: false, 65 hasValueName: false, 66 terminalColumns: getTerminalColumns(), 67 } 68 69 if ret.terminalColumns <= 0 { 70 ret.terminalColumns = 80 71 } 72 73 var prevcmd *Command 74 75 p.eachActiveGroup(func(c *Command, grp *Group) { 76 if c != prevcmd { 77 for _, arg := range c.args { 78 ret.updateLen(arg.Name, c != p.Command) 79 } 80 } 81 82 for _, info := range grp.options { 83 if !info.canCli() { 84 continue 85 } 86 87 if info.ShortName != 0 { 88 ret.hasShort = true 89 } 90 91 if len(info.ValueName) > 0 { 92 ret.hasValueName = true 93 } 94 95 ret.updateLen(info.LongNameWithNamespace()+info.ValueName, c != p.Command) 96 } 97 }) 98 99 return ret 100} 101 102func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { 103 line := &bytes.Buffer{} 104 105 prefix := paddingBeforeOption 106 107 if info.indent { 108 prefix += 4 109 } 110 111 line.WriteString(strings.Repeat(" ", prefix)) 112 113 if option.ShortName != 0 { 114 line.WriteRune(defaultShortOptDelimiter) 115 line.WriteRune(option.ShortName) 116 } else if info.hasShort { 117 line.WriteString(" ") 118 } 119 120 descstart := info.descriptionStart() + paddingBeforeOption 121 122 if len(option.LongName) > 0 { 123 if option.ShortName != 0 { 124 line.WriteString(", ") 125 } else if info.hasShort { 126 line.WriteString(" ") 127 } 128 129 line.WriteString(defaultLongOptDelimiter) 130 line.WriteString(option.LongNameWithNamespace()) 131 } 132 133 if option.canArgument() { 134 line.WriteRune(defaultNameArgDelimiter) 135 136 if len(option.ValueName) > 0 { 137 line.WriteString(option.ValueName) 138 } 139 } 140 141 written := line.Len() 142 line.WriteTo(writer) 143 144 if option.Description != "" { 145 dw := descstart - written 146 writer.WriteString(strings.Repeat(" ", dw)) 147 148 def := "" 149 defs := option.Default 150 151 if len(option.DefaultMask) != 0 { 152 if option.DefaultMask != "-" { 153 def = option.DefaultMask 154 } 155 } else if len(defs) == 0 && option.canArgument() { 156 var showdef bool 157 158 switch option.field.Type.Kind() { 159 case reflect.Func, reflect.Ptr: 160 showdef = !option.value.IsNil() 161 case reflect.Slice, reflect.String, reflect.Array: 162 showdef = option.value.Len() > 0 163 case reflect.Map: 164 showdef = !option.value.IsNil() && option.value.Len() > 0 165 default: 166 zeroval := reflect.Zero(option.field.Type) 167 showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) 168 } 169 170 if showdef { 171 def, _ = convertToString(option.value, option.tag) 172 } 173 } else if len(defs) != 0 { 174 l := len(defs) - 1 175 176 for i := 0; i < l; i++ { 177 def += quoteIfNeeded(defs[i]) + ", " 178 } 179 180 def += quoteIfNeeded(defs[l]) 181 } 182 183 var envDef string 184 if option.EnvDefaultKey != "" { 185 var envPrintable string 186 if runtime.GOOS == "windows" { 187 envPrintable = "%" + option.EnvDefaultKey + "%" 188 } else { 189 envPrintable = "$" + option.EnvDefaultKey 190 } 191 envDef = fmt.Sprintf(" [%s]", envPrintable) 192 } 193 194 var desc string 195 196 if def != "" { 197 desc = fmt.Sprintf("%s (%v)%s", option.Description, def, envDef) 198 } else { 199 desc = option.Description + envDef 200 } 201 202 writer.WriteString(wrapText(desc, 203 info.terminalColumns-descstart, 204 strings.Repeat(" ", descstart))) 205 } 206 207 writer.WriteString("\n") 208} 209 210func maxCommandLength(s []*Command) int { 211 if len(s) == 0 { 212 return 0 213 } 214 215 ret := len(s[0].Name) 216 217 for _, v := range s[1:] { 218 l := len(v.Name) 219 220 if l > ret { 221 ret = l 222 } 223 } 224 225 return ret 226} 227 228// WriteHelp writes a help message containing all the possible options and 229// their descriptions to the provided writer. Note that the HelpFlag parser 230// option provides a convenient way to add a -h/--help option group to the 231// command line parser which will automatically show the help messages using 232// this method. 233func (p *Parser) WriteHelp(writer io.Writer) { 234 if writer == nil { 235 return 236 } 237 238 wr := bufio.NewWriter(writer) 239 aligninfo := p.getAlignmentInfo() 240 241 cmd := p.Command 242 243 for cmd.Active != nil { 244 cmd = cmd.Active 245 } 246 247 if p.Name != "" { 248 wr.WriteString("Usage:\n") 249 wr.WriteString(" ") 250 251 allcmd := p.Command 252 253 for allcmd != nil { 254 var usage string 255 256 if allcmd == p.Command { 257 if len(p.Usage) != 0 { 258 usage = p.Usage 259 } else if p.Options&HelpFlag != 0 { 260 usage = "[OPTIONS]" 261 } 262 } else if us, ok := allcmd.data.(Usage); ok { 263 usage = us.Usage() 264 } else if allcmd.hasCliOptions() { 265 usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) 266 } 267 268 if len(usage) != 0 { 269 fmt.Fprintf(wr, " %s %s", allcmd.Name, usage) 270 } else { 271 fmt.Fprintf(wr, " %s", allcmd.Name) 272 } 273 274 if len(allcmd.args) > 0 { 275 fmt.Fprintf(wr, " ") 276 } 277 278 for i, arg := range allcmd.args { 279 if i != 0 { 280 fmt.Fprintf(wr, " ") 281 } 282 283 name := arg.Name 284 285 if arg.isRemaining() { 286 name = name + "..." 287 } 288 289 if !allcmd.ArgsRequired { 290 fmt.Fprintf(wr, "[%s]", name) 291 } else { 292 fmt.Fprintf(wr, "%s", name) 293 } 294 } 295 296 if allcmd.Active == nil && len(allcmd.commands) > 0 { 297 var co, cc string 298 299 if allcmd.SubcommandsOptional { 300 co, cc = "[", "]" 301 } else { 302 co, cc = "<", ">" 303 } 304 305 if len(allcmd.commands) > 3 { 306 fmt.Fprintf(wr, " %scommand%s", co, cc) 307 } else { 308 subcommands := allcmd.sortedCommands() 309 names := make([]string, len(subcommands)) 310 311 for i, subc := range subcommands { 312 names[i] = subc.Name 313 } 314 315 fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc) 316 } 317 } 318 319 allcmd = allcmd.Active 320 } 321 322 fmt.Fprintln(wr) 323 324 if len(cmd.LongDescription) != 0 { 325 fmt.Fprintln(wr) 326 327 t := wrapText(cmd.LongDescription, 328 aligninfo.terminalColumns, 329 "") 330 331 fmt.Fprintln(wr, t) 332 } 333 } 334 335 c := p.Command 336 337 for c != nil { 338 printcmd := c != p.Command 339 340 c.eachGroup(func(grp *Group) { 341 first := true 342 343 // Skip built-in help group for all commands except the top-level 344 // parser 345 if grp.isBuiltinHelp && c != p.Command { 346 return 347 } 348 349 for _, info := range grp.options { 350 if !info.canCli() { 351 continue 352 } 353 354 if printcmd { 355 fmt.Fprintf(wr, "\n[%s command options]\n", c.Name) 356 aligninfo.indent = true 357 printcmd = false 358 } 359 360 if first && cmd.Group != grp { 361 fmt.Fprintln(wr) 362 363 if aligninfo.indent { 364 wr.WriteString(" ") 365 } 366 367 fmt.Fprintf(wr, "%s:\n", grp.ShortDescription) 368 first = false 369 } 370 371 p.writeHelpOption(wr, info, aligninfo) 372 } 373 }) 374 375 if len(c.args) > 0 { 376 if c == p.Command { 377 fmt.Fprintf(wr, "\nArguments:\n") 378 } else { 379 fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name) 380 } 381 382 maxlen := aligninfo.descriptionStart() 383 384 for _, arg := range c.args { 385 prefix := strings.Repeat(" ", paddingBeforeOption) 386 fmt.Fprintf(wr, "%s%s", prefix, arg.Name) 387 388 if len(arg.Description) > 0 { 389 align := strings.Repeat(" ", maxlen-len(arg.Name)-1) 390 fmt.Fprintf(wr, ":%s%s", align, arg.Description) 391 } 392 393 fmt.Fprintln(wr) 394 } 395 } 396 397 c = c.Active 398 } 399 400 scommands := cmd.sortedCommands() 401 402 if len(scommands) > 0 { 403 maxnamelen := maxCommandLength(scommands) 404 405 fmt.Fprintln(wr) 406 fmt.Fprintln(wr, "Available commands:") 407 408 for _, c := range scommands { 409 fmt.Fprintf(wr, " %s", c.Name) 410 411 if len(c.ShortDescription) > 0 { 412 pad := strings.Repeat(" ", maxnamelen-len(c.Name)) 413 fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) 414 415 if len(c.Aliases) > 0 { 416 fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) 417 } 418 419 } 420 421 fmt.Fprintln(wr) 422 } 423 } 424 425 wr.Flush() 426} 427