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- } 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 }