// Copyright 2012 Jesse van den Kieboom. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package flags import ( "bufio" "bytes" "fmt" "io" "runtime" "strings" "unicode/utf8" ) type alignmentInfo struct { maxLongLen int hasShort bool hasValueName bool terminalColumns int indent bool } const ( paddingBeforeOption = 2 distanceBetweenOptionAndDescription = 2 ) func (a *alignmentInfo) descriptionStart() int { ret := a.maxLongLen + distanceBetweenOptionAndDescription if a.hasShort { ret += 2 } if a.maxLongLen > 0 { ret += 4 } if a.hasValueName { ret += 3 } return ret } func (a *alignmentInfo) updateLen(name string, indent bool) { l := utf8.RuneCountInString(name) if indent { l = l + 4 } if l > a.maxLongLen { a.maxLongLen = l } } func (p *Parser) getAlignmentInfo() alignmentInfo { ret := alignmentInfo{ maxLongLen: 0, hasShort: false, hasValueName: false, terminalColumns: getTerminalColumns(), } if ret.terminalColumns <= 0 { ret.terminalColumns = 80 } var prevcmd *Command p.eachActiveGroup(func(c *Command, grp *Group) { if c != prevcmd { for _, arg := range c.args { ret.updateLen(arg.Name, c != p.Command) } } for _, info := range grp.options { if !info.canCli() { continue } if info.ShortName != 0 { ret.hasShort = true } if len(info.ValueName) > 0 { ret.hasValueName = true } l := info.LongNameWithNamespace() + info.ValueName if len(info.Choices) != 0 { l += "[" + strings.Join(info.Choices, "|") + "]" } ret.updateLen(l, c != p.Command) } }) return ret } func wrapText(s string, l int, prefix string) string { var ret string if l < 10 { l = 10 } // Basic text wrapping of s at spaces to fit in l lines := strings.Split(s, "\n") for _, line := range lines { var retline string line = strings.TrimSpace(line) for len(line) > l { // Try to split on space suffix := "" pos := strings.LastIndex(line[:l], " ") if pos < 0 { pos = l - 1 suffix = "-\n" } if len(retline) != 0 { retline += "\n" + prefix } retline += strings.TrimSpace(line[:pos]) + suffix line = strings.TrimSpace(line[pos:]) } if len(line) > 0 { if len(retline) != 0 { retline += "\n" + prefix } retline += line } if len(ret) > 0 { ret += "\n" if len(retline) > 0 { ret += prefix } } ret += retline } return ret } func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { line := &bytes.Buffer{} prefix := paddingBeforeOption if info.indent { prefix += 4 } if option.Hidden { return } line.WriteString(strings.Repeat(" ", prefix)) if option.ShortName != 0 { line.WriteRune(defaultShortOptDelimiter) line.WriteRune(option.ShortName) } else if info.hasShort { line.WriteString(" ") } descstart := info.descriptionStart() + paddingBeforeOption if len(option.LongName) > 0 { if option.ShortName != 0 { line.WriteString(", ") } else if info.hasShort { line.WriteString(" ") } line.WriteString(defaultLongOptDelimiter) line.WriteString(option.LongNameWithNamespace()) } if option.canArgument() { line.WriteRune(defaultNameArgDelimiter) if len(option.ValueName) > 0 { line.WriteString(option.ValueName) } if len(option.Choices) > 0 { line.WriteString("[" + strings.Join(option.Choices, "|") + "]") } } written := line.Len() line.WriteTo(writer) if option.Description != "" { dw := descstart - written writer.WriteString(strings.Repeat(" ", dw)) var def string if len(option.DefaultMask) != 0 { if option.DefaultMask != "-" { def = option.DefaultMask } } else { def = option.defaultLiteral } var envDef string if option.EnvDefaultKey != "" { var envPrintable string if runtime.GOOS == "windows" { envPrintable = "%" + option.EnvDefaultKey + "%" } else { envPrintable = "$" + option.EnvDefaultKey } envDef = fmt.Sprintf(" [%s]", envPrintable) } var desc string if def != "" { desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef) } else { desc = option.Description + envDef } writer.WriteString(wrapText(desc, info.terminalColumns-descstart, strings.Repeat(" ", descstart))) } writer.WriteString("\n") } func maxCommandLength(s []*Command) int { if len(s) == 0 { return 0 } ret := len(s[0].Name) for _, v := range s[1:] { l := len(v.Name) if l > ret { ret = l } } return ret } // WriteHelp writes a help message containing all the possible options and // their descriptions to the provided writer. Note that the HelpFlag parser // option provides a convenient way to add a -h/--help option group to the // command line parser which will automatically show the help messages using // this method. func (p *Parser) WriteHelp(writer io.Writer) { if writer == nil { return } wr := bufio.NewWriter(writer) aligninfo := p.getAlignmentInfo() cmd := p.Command for cmd.Active != nil { cmd = cmd.Active } if p.Name != "" { wr.WriteString("Usage:\n") wr.WriteString(" ") allcmd := p.Command for allcmd != nil { var usage string if allcmd == p.Command { if len(p.Usage) != 0 { usage = p.Usage } else if p.Options&HelpFlag != 0 { usage = "[OPTIONS]" } } else if us, ok := allcmd.data.(Usage); ok { usage = us.Usage() } else if allcmd.hasCliOptions() { usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) } if len(usage) != 0 { fmt.Fprintf(wr, " %s %s", allcmd.Name, usage) } else { fmt.Fprintf(wr, " %s", allcmd.Name) } if len(allcmd.args) > 0 { fmt.Fprintf(wr, " ") } for i, arg := range allcmd.args { if i != 0 { fmt.Fprintf(wr, " ") } name := arg.Name if arg.isRemaining() { name = name + "..." } if !allcmd.ArgsRequired { fmt.Fprintf(wr, "[%s]", name) } else { fmt.Fprintf(wr, "%s", name) } } if allcmd.Active == nil && len(allcmd.commands) > 0 { var co, cc string if allcmd.SubcommandsOptional { co, cc = "[", "]" } else { co, cc = "<", ">" } visibleCommands := allcmd.visibleCommands() if len(visibleCommands) > 3 { fmt.Fprintf(wr, " %scommand%s", co, cc) } else { subcommands := allcmd.sortedVisibleCommands() names := make([]string, len(subcommands)) for i, subc := range subcommands { names[i] = subc.Name } fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc) } } allcmd = allcmd.Active } fmt.Fprintln(wr) if len(cmd.LongDescription) != 0 { fmt.Fprintln(wr) t := wrapText(cmd.LongDescription, aligninfo.terminalColumns, "") fmt.Fprintln(wr, t) } } c := p.Command for c != nil { printcmd := c != p.Command c.eachGroup(func(grp *Group) { first := true // Skip built-in help group for all commands except the top-level // parser if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { return } for _, info := range grp.options { if !info.canCli() || info.Hidden { continue } if printcmd { fmt.Fprintf(wr, "\n[%s command options]\n", c.Name) aligninfo.indent = true printcmd = false } if first && cmd.Group != grp { fmt.Fprintln(wr) if aligninfo.indent { wr.WriteString(" ") } fmt.Fprintf(wr, "%s:\n", grp.ShortDescription) first = false } p.writeHelpOption(wr, info, aligninfo) } }) var args []*Arg for _, arg := range c.args { if arg.Description != "" { args = append(args, arg) } } if len(args) > 0 { if c == p.Command { fmt.Fprintf(wr, "\nArguments:\n") } else { fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name) } descStart := aligninfo.descriptionStart() + paddingBeforeOption for _, arg := range args { argPrefix := strings.Repeat(" ", paddingBeforeOption) argPrefix += arg.Name if len(arg.Description) > 0 { argPrefix += ":" wr.WriteString(argPrefix) // Space between "arg:" and the description start descPadding := strings.Repeat(" ", descStart-len(argPrefix)) // How much space the description gets before wrapping descWidth := aligninfo.terminalColumns - 1 - descStart // Whitespace to which we can indent new description lines descPrefix := strings.Repeat(" ", descStart) wr.WriteString(descPadding) wr.WriteString(wrapText(arg.Description, descWidth, descPrefix)) } else { wr.WriteString(argPrefix) } fmt.Fprintln(wr) } } c = c.Active } scommands := cmd.sortedVisibleCommands() if len(scommands) > 0 { maxnamelen := maxCommandLength(scommands) fmt.Fprintln(wr) fmt.Fprintln(wr, "Available commands:") for _, c := range scommands { fmt.Fprintf(wr, " %s", c.Name) if len(c.ShortDescription) > 0 { pad := strings.Repeat(" ", maxnamelen-len(c.Name)) fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) if len(c.Aliases) > 0 { fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) } } fmt.Fprintln(wr) } } wr.Flush() }