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 }