| // 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 && 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() |
| } |