regolancer/helpmessage/message.go

304 lines
5.1 KiB
Go
Raw Normal View History

2022-11-14 00:04:47 +00:00
package helpmessage
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"reflect"
"strings"
"unicode/utf8"
"github.com/jessevdk/go-flags"
)
const (
paddingBeforeOption = 2
distanceBetweenOptionAndDescription = 2
defaultShortOptDelimiter = '-'
defaultLongOptDelimiter = "--"
defaultNameArgDelimiter = '='
)
type Option struct {
Description string
ShortName rune
LongName string
Grouping string
field reflect.StructField
value reflect.Value
}
type Options struct {
options []*Option
}
2022-11-14 00:04:47 +00:00
func ScanStruct(realval reflect.Value, opt *Options) error {
stype := realval.Type()
for i := 0; i < stype.NumField(); i++ {
field := stype.Field(i)
longname := field.Tag.Get("long")
shortname := field.Tag.Get("short")
grouping := field.Tag.Get("rego-grouping")
description := field.Tag.Get("description")
// Need at least either a short or long name
if longname == "" && shortname == "" {
continue
}
short := rune(0)
rc := utf8.RuneCountInString(shortname)
if rc > 1 {
return fmt.Errorf("short names can only be 1 character long, not `%s'",
shortname)
} else if rc == 1 {
short, _ = utf8.DecodeRuneInString(shortname)
}
option := &Option{
Description: description,
ShortName: short,
LongName: longname,
Grouping: grouping,
field: field,
value: realval.Field(i),
}
opt.options = append(opt.options, option)
}
return nil
}
func WriteHelp(options *Options, p *flags.Parser, writer io.Writer) (err error) {
if writer == nil {
return
}
wr := bufio.NewWriter(os.Stdout)
aligninfo := getAlignmentInfo(options, p)
if p.Name != "" {
wr.WriteString("Usage:\n")
wr.WriteString(" ")
fmt.Fprintf(wr, " %s [OPTIONS]\n", p.Name)
}
fmt.Fprintf(wr, "\nApplication Options:\n")
fmt.Fprintf(wr, "--------------------")
aligninfo.indent = false
for _, info := range options.options {
if !info.showInHelp() {
continue
}
writeHelpOption(wr, info, aligninfo)
}
wr.Flush()
return nil
}
// }
func getAlignmentInfo(options *Options, p *flags.Parser) alignmentInfo {
ret := alignmentInfo{
maxLongLen: 0,
hasShort: false,
hasValueName: false,
terminalColumns: getTerminalColumns(),
}
if ret.terminalColumns <= 0 {
ret.terminalColumns = 80
}
for _, info := range options.options {
if !info.showInHelp() {
continue
}
if info.ShortName != 0 {
ret.hasShort = true
}
l := info.LongName
ret.updateLen(l, false)
}
return ret
}
type alignmentInfo struct {
maxLongLen int
hasShort bool
hasValueName bool
terminalColumns int
indent bool
}
const (
HelpFlag = 1
)
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 (option *Option) showInHelp() bool {
return (option.ShortName != 0 || len(option.LongName) != 0)
}
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
}
}
var group = make(map[string]bool)
func writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
line := &bytes.Buffer{}
if _, ok := group[option.Grouping]; !ok && option.Grouping != "" {
writer.WriteString(fmt.Sprintf("\n%s:\n", option.Grouping))
group[option.Grouping] = true
}
prefix := paddingBeforeOption
if info.indent {
prefix += 4
}
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.LongName)
}
written := line.Len()
line.WriteTo(writer)
if option.Description != "" {
dw := descstart - written
writer.WriteString(strings.Repeat(" ", dw))
desc := option.Description
writer.WriteString(wrapText(desc,
info.terminalColumns-descstart,
strings.Repeat(" ", descstart)))
}
writer.WriteString("\n")
}