Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
491 lines
9.1 KiB
491 lines
9.1 KiB
// 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() |
|
}
|
|
|