fix: handle contents of tags properly by unquoting them when necessary

This commit is contained in:
Florian Loch
2023-01-25 09:20:43 +01:00
committed by Alec Thomas
parent 95a465b4b5
commit 37e801405f
3 changed files with 51 additions and 10 deletions
+2 -1
View File
@@ -8,8 +8,9 @@ import (
"testing" "testing"
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
"github.com/alecthomas/kong"
"github.com/alecthomas/repr" "github.com/alecthomas/repr"
"github.com/alecthomas/kong"
) )
func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong { func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong {
+34 -7
View File
@@ -56,11 +56,13 @@ func (t *Tag) String() string {
type tagChars struct { type tagChars struct {
sep, quote, assign rune sep, quote, assign rune
needsUnquote bool
} }
var kongChars = tagChars{sep: ',', quote: '\'', assign: '='} var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'} var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
// nolint:gocyclo
func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) { func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
d := map[string][]string{} d := map[string][]string{}
key := []rune{} key := []rune{}
@@ -68,11 +70,25 @@ func parseTagItems(tagString string, chr tagChars) (map[string][]string, error)
quotes := false quotes := false
inKey := true inKey := true
add := func() { add := func() error {
d[string(key)] = append(d[string(key)], string(value)) // 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{} key = []rune{}
value = []rune{} value = []rune{}
inKey = true inKey = true
return nil
} }
runes := []rune(tagString) runes := []rune(tagString)
@@ -86,7 +102,10 @@ func parseTagItems(tagString string, chr tagChars) (map[string][]string, error)
eof = true eof = true
} }
if !quotes && r == chr.sep { if !quotes && r == chr.sep {
add() if err := add(); err != nil {
return nil, err
}
continue continue
} }
if r == chr.assign && inKey { if r == chr.assign && inKey {
@@ -96,6 +115,12 @@ func parseTagItems(tagString string, chr tagChars) (map[string][]string, error)
if r == '\\' { if r == '\\' {
if next == chr.quote { if next == chr.quote {
idx++ idx++
// We need to keep the backslashes, otherwise subsequent unquoting cannot work
if chr.needsUnquote {
value = append(value, r)
}
r = chr.quote r = chr.quote
} }
} else if r == chr.quote { } else if r == chr.quote {
@@ -119,7 +144,9 @@ func parseTagItems(tagString string, chr tagChars) (map[string][]string, error)
return nil, fmt.Errorf("%v is not quoted properly", tagString) return nil, fmt.Errorf("%v is not quoted properly", tagString)
} }
add() if err := add(); err != nil {
return nil, err
}
return d, nil return d, nil
} }
@@ -242,7 +269,7 @@ func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
} }
t.PlaceHolder = t.Get("placeholder") t.PlaceHolder = t.Get("placeholder")
t.Enum = t.Get("enum") t.Enum = t.Get("enum")
scalarType := (typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr)) scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr)
if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType { 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") return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
} }
+15 -2
View File
@@ -5,17 +5,18 @@ import (
"testing" "testing"
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
) )
func TestDefaultValueForOptionalArg(t *testing.T) { func TestDefaultValueForOptionalArg(t *testing.T) {
var cli struct { var cli struct {
Arg string `kong:"arg,optional,default='👌'"` Arg string `kong:"arg,optional,default='\"\\'👌\\'\"'"`
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)
_, err := p.Parse(nil) _, err := p.Parse(nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "👌", cli.Arg) assert.Equal(t, "\"'👌'\"", cli.Arg)
} }
func TestNoValueInTag(t *testing.T) { func TestNoValueInTag(t *testing.T) {
@@ -66,6 +67,18 @@ func TestEscapedQuote(t *testing.T) {
assert.Equal(t, "i don't know", cli.DoYouKnow) assert.Equal(t, "i don't know", cli.DoYouKnow)
} }
func TestEscapingInQuotedTags(t *testing.T) {
var cli struct {
Regex1 string `kong:"default='\\d+\n'"`
Regex2 string `default:"\\d+\n"`
}
p := mustNew(t, &cli)
_, err := p.Parse(nil)
assert.NoError(t, err)
assert.Equal(t, "\\d+\n", cli.Regex1)
assert.Equal(t, "\\d+\n", cli.Regex2)
}
func TestBareTags(t *testing.T) { func TestBareTags(t *testing.T) {
var cli struct { var cli struct {
Cmd struct { Cmd struct {