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.
388 lines
7.9 KiB
388 lines
7.9 KiB
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/mattn/go-isatty"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
colorRE = regexp.MustCompile(`\x1b[;?[0-9]+m`)
|
|
defaultShowOptions = &ShowOptions{Colors: true, LineWidth: 80, Titles: true}
|
|
)
|
|
|
|
type ShowWriter interface {
|
|
io.Writer
|
|
Fd() uintptr
|
|
}
|
|
|
|
type ShowOptions struct {
|
|
Colors bool
|
|
Types bool
|
|
Long bool
|
|
Titles bool
|
|
LineWidth int
|
|
Writer ShowWriter
|
|
}
|
|
|
|
func length(str string) int {
|
|
return len(colorRE.ReplaceAllString(str, ""))
|
|
}
|
|
|
|
func colorize(enable bool, color int, str string) string {
|
|
if enable {
|
|
return fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", color, str)
|
|
}
|
|
return str
|
|
}
|
|
|
|
func Blue(str string, opts ...*ShowOptions) string {
|
|
return colorize(resolveShowOptions(opts).Colors, 34, str)
|
|
}
|
|
|
|
func Red(str string, opts ...*ShowOptions) string {
|
|
return colorize(resolveShowOptions(opts).Colors, 31, str)
|
|
}
|
|
|
|
func Yellow(str string, opts ...*ShowOptions) string {
|
|
return colorize(resolveShowOptions(opts).Colors, 33, str)
|
|
}
|
|
|
|
func Cyan(str string, opts ...*ShowOptions) string {
|
|
return colorize(resolveShowOptions(opts).Colors, 35, str)
|
|
}
|
|
|
|
func Green(str string, opts ...*ShowOptions) string {
|
|
return colorize(resolveShowOptions(opts).Colors, 32, str)
|
|
}
|
|
|
|
func resolveShowOptions(opts []*ShowOptions) *ShowOptions {
|
|
for _, opt := range opts {
|
|
if opt.LineWidth == 0 {
|
|
opt.LineWidth = 80
|
|
}
|
|
return opt
|
|
}
|
|
return defaultShowOptions
|
|
}
|
|
|
|
func showMessage(opts *ShowOptions, f func(...*ShowOptions) string) error {
|
|
out := opts.Writer
|
|
if out == nil {
|
|
out = os.Stdout
|
|
}
|
|
if opts.Colors && (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(out.Fd()) && !isatty.IsCygwinTerminal(out.Fd()))) {
|
|
opts.Colors = false
|
|
}
|
|
_, err := fmt.Fprintln(out, f(opts))
|
|
return err
|
|
}
|
|
|
|
func versionToString(cmd *Command, opts *ShowOptions) string {
|
|
var str string
|
|
if len(cmd.version.RuntimeValue) > 0 {
|
|
str = cmd.version.RuntimeValue[0]
|
|
} else {
|
|
str = cmd.version.DefaultValue.(string)
|
|
}
|
|
if opts.Colors {
|
|
str = Green(str, opts)
|
|
}
|
|
if opts.Titles {
|
|
str = "VERSION: " + str
|
|
}
|
|
return str
|
|
}
|
|
|
|
func usageToString(cmd *Command, opts *ShowOptions) string {
|
|
var str string
|
|
if opts.Titles {
|
|
str += "USAGE:"
|
|
}
|
|
if cmd.program != nil {
|
|
str += " " + Cyan(cmd.program.name, opts)
|
|
}
|
|
str += " " + Cyan(cmd.name, opts)
|
|
if len(cmd.options) > 0 {
|
|
str += " " + Yellow("[", opts) + Cyan("OPTIONS", opts) + Yellow("]", opts)
|
|
}
|
|
for _, a := range cmd.arguments {
|
|
str += " " + argumentToString(a, opts)
|
|
}
|
|
return strings.TrimSpace(str)
|
|
}
|
|
|
|
func argumentToString(a *Argument, opts *ShowOptions) string {
|
|
var str string
|
|
if a.Required {
|
|
str += Yellow("<", opts)
|
|
} else {
|
|
str += Yellow("[", opts)
|
|
}
|
|
str += Cyan(a.Name, opts)
|
|
if opts.Types && len(a.Type) > 0 {
|
|
str += ":"
|
|
if a.Variadic {
|
|
str += "..."
|
|
}
|
|
str += Green(a.Type, opts)
|
|
}
|
|
if a.Required {
|
|
str += Yellow(">", opts)
|
|
} else {
|
|
str += Yellow("]", opts)
|
|
}
|
|
return str
|
|
}
|
|
|
|
func helpToString(cmd *Command, opts *ShowOptions) string {
|
|
var str string
|
|
if usage := cmd.GetUsage(opts); len(usage) > 0 {
|
|
str += usage + "\n"
|
|
}
|
|
if version := cmd.GetVersion(opts); len(version) > 0 {
|
|
str += version + "\n"
|
|
}
|
|
if desc := cmd.GetDescription(opts); len(desc) > 0 {
|
|
str += "\n" + desc + "\n\n"
|
|
}
|
|
if options := optionsToString(cmd, opts); len(options) > 0 {
|
|
str += options + "\n"
|
|
}
|
|
if arguments := argumentsToString(cmd, opts); len(arguments) > 0 {
|
|
str += arguments + "\n"
|
|
}
|
|
if commands := commandsToString(cmd, opts); len(commands) > 0 {
|
|
str += commands + "\n"
|
|
}
|
|
return strings.TrimSpace(str) + "\n"
|
|
}
|
|
|
|
func commandsToString(cmd *Command, opts *ShowOptions) string {
|
|
if len(cmd.commands) == 0 {
|
|
return ""
|
|
}
|
|
tb := &table{
|
|
gaps: []string{" ", ""},
|
|
rows: [][]string{},
|
|
sizes: []int{0, 0},
|
|
}
|
|
for _, o := range cmd.commands {
|
|
tb.AddRow(
|
|
Cyan(o.name, opts),
|
|
o.description,
|
|
)
|
|
}
|
|
str := tb.String(" ", "", opts.LineWidth)
|
|
if opts.Titles {
|
|
return "SUBCOMMANDS:\n" + str
|
|
}
|
|
return str
|
|
}
|
|
|
|
func argumentsToString(cmd *Command, opts *ShowOptions) string {
|
|
if len(cmd.arguments) == 0 {
|
|
return ""
|
|
}
|
|
t := &table{
|
|
gaps: []string{" ", " ", ""},
|
|
sizes: []int{0, 0, 0},
|
|
rows: [][]string{},
|
|
}
|
|
if !opts.Types {
|
|
t.gaps[1] = ""
|
|
}
|
|
for _, a := range cmd.arguments {
|
|
var typ string
|
|
if opts.Types && len(a.Type) > 0 {
|
|
if a.Required {
|
|
typ += Yellow("<", opts)
|
|
} else {
|
|
typ += Yellow("[", opts)
|
|
}
|
|
typ += Cyan(a.Type, opts)
|
|
if a.Required {
|
|
typ += Yellow("<", opts)
|
|
} else {
|
|
typ += Yellow("[", opts)
|
|
}
|
|
}
|
|
t.AddRow(
|
|
Cyan(a.Name, opts),
|
|
typ,
|
|
a.Description,
|
|
)
|
|
}
|
|
if opts.Titles {
|
|
return "ARGUMENTS:\n" + t.String(" ", "", opts.LineWidth) + "\n"
|
|
}
|
|
return t.String(" ", "", opts.LineWidth) + "\n"
|
|
}
|
|
|
|
func optionsToString(cmd *Command, opts *ShowOptions) string {
|
|
if len(cmd.options) == 0 {
|
|
return ""
|
|
}
|
|
t := &table{
|
|
gaps: []string{" ", " ", ""},
|
|
sizes: []int{0, 0, 0},
|
|
rows: [][]string{},
|
|
}
|
|
for _, o := range cmd.options {
|
|
t.AddRow(
|
|
optionFlagsToString(o, opts),
|
|
argumentToString(o.toArgument(), opts),
|
|
o.Description,
|
|
)
|
|
}
|
|
str := t.String(" ", "", opts.LineWidth)
|
|
if opts.Titles {
|
|
return "OPTIONS:\n" + str + "\n"
|
|
}
|
|
return str + "\n"
|
|
}
|
|
|
|
func optionFlagsToString(o *Option, opts *ShowOptions) string {
|
|
var str string
|
|
if len(o.Short) > 0 {
|
|
str += Blue("-"+o.Short, opts) + ", "
|
|
} else {
|
|
// 保留 short 的位置
|
|
str += " "
|
|
}
|
|
return str + Blue("--"+o.Long, opts)
|
|
}
|
|
|
|
func ensureOption(c *Command, expr string, desc []string, owner string) (*Option, error) {
|
|
o1, err := createOption(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch len(desc) {
|
|
case 0:
|
|
break
|
|
case 1:
|
|
o1.Description = desc[0]
|
|
default:
|
|
o1.Description = desc[0]
|
|
o1.LongDescription = desc[1]
|
|
}
|
|
for _, o2 := range c.options {
|
|
err := hasConflicts(o2, o1)
|
|
if err != nil {
|
|
if len(owner) > 0 {
|
|
return nil, errors.New(owner + " option conflicts with an optional parameter")
|
|
}
|
|
return nil, errors.New("option conflicts with another optional parameter")
|
|
}
|
|
}
|
|
return o1, nil
|
|
}
|
|
|
|
type table struct {
|
|
gaps []string
|
|
sizes []int
|
|
rows [][]string
|
|
}
|
|
|
|
var spaceRE = regexp.MustCompile(`[\t\n\v\f\r]+`)
|
|
var noNewlineSpaceRE = regexp.MustCompile(`[\t\v\f\r]+]`)
|
|
|
|
func (t *table) AddRow(columns ...string) {
|
|
l := len(columns)
|
|
width := len(t.sizes)
|
|
for i, column := range columns {
|
|
column = strings.TrimSpace(column)
|
|
if i < l-1 {
|
|
column = spaceRE.ReplaceAllString(column, "")
|
|
} else {
|
|
column = noNewlineSpaceRE.ReplaceAllString(column, "")
|
|
}
|
|
columns[i] = column
|
|
size := length(column)
|
|
if size > t.sizes[i] {
|
|
t.sizes[i] = size
|
|
}
|
|
}
|
|
for i := l; i < width; i++ {
|
|
columns = append(columns, "")
|
|
}
|
|
t.rows = append(t.rows, columns)
|
|
}
|
|
|
|
func (t *table) String(linePrefix, lineSuffix string, lineWidth int) string {
|
|
lastColumnIndex := len(t.sizes) - 1
|
|
lastColumnWidth := 0
|
|
|
|
var newlinePrefix string
|
|
var str string
|
|
|
|
for j, row := range t.rows {
|
|
line := linePrefix
|
|
for i, col := range row {
|
|
width := t.sizes[i]
|
|
// 不是最后一行
|
|
if i < lastColumnIndex {
|
|
line += col
|
|
count := length(col)
|
|
if count < width {
|
|
line += strings.Repeat(" ", width-count)
|
|
}
|
|
line += t.gaps[i]
|
|
continue
|
|
}
|
|
|
|
// 最后一行换行
|
|
if lastColumnWidth == 0 {
|
|
lastColumnWidth = lineWidth - length(line) - length(lineSuffix)
|
|
if lastColumnWidth < 10 {
|
|
lastColumnWidth = 10
|
|
}
|
|
newlinePrefix = strings.Repeat(" ", length(line))
|
|
}
|
|
|
|
for _, cline := range strings.Split(col, "\n") {
|
|
var s string
|
|
var w int
|
|
var x int
|
|
var y int
|
|
for _, word := range strings.Split(cline, " ") {
|
|
n := length(word)
|
|
if w+n > lastColumnWidth {
|
|
if y > 0 {
|
|
line += "\n" + newlinePrefix
|
|
}
|
|
line += s
|
|
y += 1
|
|
s = ""
|
|
w = 0
|
|
x = 0
|
|
continue
|
|
}
|
|
if x > 0 {
|
|
s += " "
|
|
}
|
|
s += word
|
|
w += n
|
|
x += 1
|
|
}
|
|
if x > 0 {
|
|
if y > 0 {
|
|
line += "\n" + newlinePrefix
|
|
}
|
|
line += s
|
|
}
|
|
}
|
|
}
|
|
if j > 0 {
|
|
str += "\n"
|
|
}
|
|
str += line
|
|
}
|
|
|
|
return str
|
|
}
|
|
|