fix: handle contents of tags properly by unquoting them when necessary
This commit is contained in:
committed by
Alec Thomas
parent
95a465b4b5
commit
37e801405f
+2
-1
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user