47b090f2f4
Fixes #343
387 lines
9.9 KiB
Go
387 lines
9.9 KiB
Go
package kong
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// PassthroughMode indicates how parameters are passed through when "passthrough" is set.
|
|
type PassthroughMode int
|
|
|
|
const (
|
|
// PassThroughModeNone indicates passthrough mode is disabled.
|
|
PassThroughModeNone PassthroughMode = iota
|
|
// PassThroughModeAll indicates that all parameters, including flags, are passed through. It is the default.
|
|
PassThroughModeAll
|
|
// PassThroughModePartial will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments.
|
|
PassThroughModePartial
|
|
)
|
|
|
|
// Tag represents the parsed state of Kong tags in a struct field tag.
|
|
type Tag struct {
|
|
Ignored bool // Field is ignored by Kong. ie. kong:"-"
|
|
Cmd bool
|
|
Arg bool
|
|
Required bool
|
|
Optional bool
|
|
Name string
|
|
Help string
|
|
Type string
|
|
TypeName string
|
|
HasDefault bool
|
|
Default string
|
|
Format string
|
|
PlaceHolder string
|
|
Envs []string
|
|
Short rune
|
|
Hidden bool
|
|
Sep rune
|
|
MapSep rune
|
|
Enum string
|
|
Group string
|
|
Xor []string
|
|
And []string
|
|
Vars Vars
|
|
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
|
|
EnvPrefix string
|
|
XorPrefix string // Optional prefix on XOR/AND groups.
|
|
Embed bool
|
|
Aliases []string
|
|
Negatable string
|
|
Passthrough bool // Deprecated: use PassthroughMode instead.
|
|
PassthroughMode PassthroughMode
|
|
|
|
// Storage for all tag keys for arbitrary lookups.
|
|
items map[string][]string
|
|
}
|
|
|
|
func (t *Tag) String() string {
|
|
out := []string{}
|
|
for key, list := range t.items {
|
|
for _, value := range list {
|
|
out = append(out, fmt.Sprintf("%s:%q", key, value))
|
|
}
|
|
}
|
|
return strings.Join(out, " ")
|
|
}
|
|
|
|
type tagChars struct {
|
|
sep, quote, assign rune
|
|
needsUnquote bool
|
|
}
|
|
|
|
var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
|
|
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
|
|
|
|
//nolint:gocyclo
|
|
func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
|
|
d := map[string][]string{}
|
|
key := []rune{}
|
|
value := []rune{}
|
|
quotes := false
|
|
inKey := true
|
|
|
|
add := func() error {
|
|
// Bare tags are quoted, therefore we need to unquote them in the same fashion reflect.Lookup() (implicitly)
|
|
// unquotes "kong tags".
|
|
s := string(value)
|
|
|
|
if chr.needsUnquote && s != "" {
|
|
if unquoted, err := strconv.Unquote(fmt.Sprintf(`"%s"`, s)); err == nil {
|
|
s = unquoted
|
|
} else {
|
|
return fmt.Errorf("unquoting tag value `%s`: %w", s, err)
|
|
}
|
|
}
|
|
|
|
d[string(key)] = append(d[string(key)], s)
|
|
key = []rune{}
|
|
value = []rune{}
|
|
inKey = true
|
|
|
|
return nil
|
|
}
|
|
|
|
runes := []rune(tagString)
|
|
for idx := 0; idx < len(runes); idx++ {
|
|
r := runes[idx]
|
|
next := rune(0)
|
|
eof := false
|
|
if idx < len(runes)-1 {
|
|
next = runes[idx+1]
|
|
} else {
|
|
eof = true
|
|
}
|
|
if !quotes && r == chr.sep {
|
|
if err := add(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
continue
|
|
}
|
|
if r == chr.assign && inKey {
|
|
inKey = false
|
|
continue
|
|
}
|
|
if r == '\\' {
|
|
if next == chr.quote {
|
|
idx++
|
|
|
|
// We need to keep the backslashes, otherwise subsequent unquoting cannot work
|
|
if chr.needsUnquote {
|
|
value = append(value, r)
|
|
}
|
|
|
|
r = chr.quote
|
|
}
|
|
} else if r == chr.quote {
|
|
if quotes {
|
|
quotes = false
|
|
if next == chr.sep || eof {
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("%v has an unexpected char at pos %v", tagString, idx)
|
|
}
|
|
quotes = true
|
|
continue
|
|
}
|
|
if inKey {
|
|
key = append(key, r)
|
|
} else {
|
|
value = append(value, r)
|
|
}
|
|
}
|
|
if quotes {
|
|
return nil, fmt.Errorf("%v is not quoted properly", tagString)
|
|
}
|
|
|
|
if err := add(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
func getTagInfo(ft reflect.StructField) (string, tagChars) {
|
|
s, ok := ft.Tag.Lookup("kong")
|
|
if ok {
|
|
return s, kongChars
|
|
}
|
|
|
|
return string(ft.Tag), bareChars
|
|
}
|
|
|
|
func newEmptyTag() *Tag {
|
|
return &Tag{items: map[string][]string{}}
|
|
}
|
|
|
|
func tagSplitFn(r rune) bool {
|
|
return r == ',' || r == ' '
|
|
}
|
|
|
|
func parseTagString(s string) (*Tag, error) {
|
|
items, err := parseTagItems(s, bareChars)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t := &Tag{
|
|
items: items,
|
|
}
|
|
err = hydrateTag(t, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %s", s, err)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
|
|
if ft.Tag.Get("kong") == "-" {
|
|
t := newEmptyTag()
|
|
t.Ignored = true
|
|
return t, nil
|
|
}
|
|
items, err := parseTagItems(getTagInfo(ft))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t := &Tag{
|
|
items: items,
|
|
}
|
|
err = hydrateTag(t, ft.Type)
|
|
if err != nil {
|
|
return nil, failField(parent, ft, "%s", err)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
|
|
var typeName string
|
|
var isBool bool
|
|
var isBoolPtr bool
|
|
if typ != nil {
|
|
typeName = typ.Name()
|
|
isBool = typ.Kind() == reflect.Bool
|
|
isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool
|
|
}
|
|
var err error
|
|
t.Cmd = t.Has("cmd")
|
|
t.Arg = t.Has("arg")
|
|
required := t.Has("required")
|
|
optional := t.Has("optional")
|
|
if required && optional {
|
|
return fmt.Errorf("can't specify both required and optional")
|
|
}
|
|
t.Required = required
|
|
t.Optional = optional
|
|
t.HasDefault = t.Has("default")
|
|
t.Default = t.Get("default")
|
|
// Arguments with defaults are always optional.
|
|
if t.Arg && t.HasDefault {
|
|
t.Optional = true
|
|
} else if t.Arg && !optional { // Arguments are required unless explicitly made optional.
|
|
t.Required = true
|
|
}
|
|
t.Name = t.Get("name")
|
|
t.Help = t.Get("help")
|
|
t.Type = t.Get("type")
|
|
t.TypeName = typeName
|
|
for _, env := range t.GetAll("env") {
|
|
t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...)
|
|
}
|
|
t.Short, err = t.GetRune("short")
|
|
if err != nil && t.Get("short") != "" {
|
|
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
|
|
}
|
|
t.Hidden = t.Has("hidden")
|
|
t.Format = t.Get("format")
|
|
t.Sep, _ = t.GetSep("sep", ',')
|
|
t.MapSep, _ = t.GetSep("mapsep", ';')
|
|
t.Group = t.Get("group")
|
|
for _, xor := range t.GetAll("xor") {
|
|
t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...)
|
|
}
|
|
for _, and := range t.GetAll("and") {
|
|
t.And = append(t.And, strings.FieldsFunc(and, tagSplitFn)...)
|
|
}
|
|
t.Prefix = t.Get("prefix")
|
|
t.EnvPrefix = t.Get("envprefix")
|
|
t.XorPrefix = t.Get("xorprefix")
|
|
t.Embed = t.Has("embed")
|
|
if t.Has("negatable") {
|
|
if !isBool && !isBoolPtr {
|
|
return fmt.Errorf("negatable can only be set on booleans")
|
|
}
|
|
negatable := t.Get("negatable")
|
|
if negatable == "" {
|
|
negatable = negatableDefault // placeholder for default negation of --no-<flag>
|
|
}
|
|
t.Negatable = negatable
|
|
}
|
|
aliases := t.Get("aliases")
|
|
if len(aliases) > 0 {
|
|
t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)
|
|
}
|
|
t.Vars = Vars{}
|
|
for _, set := range t.GetAll("set") {
|
|
parts := strings.SplitN(set, "=", 2)
|
|
if len(parts) == 0 {
|
|
return fmt.Errorf("set should be in the form key=value but got %q", set)
|
|
}
|
|
t.Vars[parts[0]] = parts[1]
|
|
}
|
|
t.PlaceHolder = t.Get("placeholder")
|
|
t.Enum = t.Get("enum")
|
|
scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr)
|
|
if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType {
|
|
return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
|
|
}
|
|
passthrough := t.Has("passthrough")
|
|
if passthrough && !t.Arg && !t.Cmd {
|
|
return fmt.Errorf("passthrough only makes sense for positional arguments or commands")
|
|
}
|
|
t.Passthrough = passthrough
|
|
if t.Passthrough {
|
|
passthroughMode := t.Get("passthrough")
|
|
switch passthroughMode {
|
|
case "partial":
|
|
t.PassthroughMode = PassThroughModePartial
|
|
case "all", "":
|
|
t.PassthroughMode = PassThroughModeAll
|
|
default:
|
|
return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Has returns true if the tag contained the given key.
|
|
func (t *Tag) Has(k string) bool {
|
|
_, ok := t.items[k]
|
|
return ok
|
|
}
|
|
|
|
// Get returns the value of the given tag.
|
|
//
|
|
// Note that this will return the empty string if the tag is missing.
|
|
func (t *Tag) Get(k string) string {
|
|
values := t.items[k]
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
// GetAll returns all encountered values for a tag, in the case of multiple occurrences.
|
|
func (t *Tag) GetAll(k string) []string {
|
|
return t.items[k]
|
|
}
|
|
|
|
// GetBool returns true if the given tag looks like a boolean truth string.
|
|
func (t *Tag) GetBool(k string) (bool, error) {
|
|
return strconv.ParseBool(t.Get(k))
|
|
}
|
|
|
|
// GetFloat parses the given tag as a float64.
|
|
func (t *Tag) GetFloat(k string) (float64, error) {
|
|
return strconv.ParseFloat(t.Get(k), 64)
|
|
}
|
|
|
|
// GetInt parses the given tag as an int64.
|
|
func (t *Tag) GetInt(k string) (int64, error) {
|
|
return strconv.ParseInt(t.Get(k), 10, 64)
|
|
}
|
|
|
|
// GetRune parses the given tag as a rune.
|
|
func (t *Tag) GetRune(k string) (rune, error) {
|
|
value := t.Get(k)
|
|
r, size := utf8.DecodeRuneInString(value)
|
|
if r == utf8.RuneError || size < len(value) {
|
|
return 0, errors.New("invalid rune")
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// GetSep parses the given tag as a rune separator, allowing for a default or none.
|
|
// The separator is returned, or -1 if "none" is specified. If the tag value is an
|
|
// invalid utf8 sequence, the default rune is returned as well as an error. If the
|
|
// tag value is more than one rune, the first rune is returned as well as an error.
|
|
func (t *Tag) GetSep(k string, dflt rune) (rune, error) {
|
|
tv := t.Get(k)
|
|
if tv == "none" {
|
|
return -1, nil
|
|
} else if tv == "" {
|
|
return dflt, nil
|
|
}
|
|
r, size := utf8.DecodeRuneInString(tv)
|
|
if r == utf8.RuneError {
|
|
return dflt, fmt.Errorf(`%v:"%v" has a rune error`, k, tv)
|
|
} else if size != len(tv) {
|
|
return r, fmt.Errorf(`%v:"%v" is more than a single rune`, k, tv)
|
|
}
|
|
return r, nil
|
|
}
|