Skip to content

Commit

Permalink
Merge pull request #6 from breml/quote-string-fixed
Browse files Browse the repository at this point in the history
Fix quote string
  • Loading branch information
breml authored Apr 17, 2021
2 parents 29344d1 + c880cdd commit 4605d71
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 65 deletions.
70 changes: 40 additions & 30 deletions ast/astutil/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,54 @@ import (

var barewordRe = regexp.MustCompile("(?s:^[A-Za-z_][A-Za-z0-9_]+$)")

// Quote returns a a string with quotes for Logstash. Supported quote types
// Quote returns a string with quotes for Logstash. Supported quote types
// are ast.DoubleQuoted, ast.SingleQuoted and ast.Bareword.
// If escape is false and the result is not a valid quoted value, an error
// is returned. If escape is true, the value will be escaped such, that the
// returned value is a valid quoted Logstash string.
// For ast.DoubleQuoted, all double quotes (`"`) are escaped to `\"`.
// For ast.SingleQuoted, all single quotes (`'`) are escaped to `\'`.
// For ast.Bareword, all characters not matching "[A-Za-z_][A-Za-z0-9_]+" are
// replaced with `_`.
func Quote(value string, quoteType ast.StringAttributeType, escape bool) (string, error) {
var hasDoubleQuote bool
var hasSingleQuote bool
// If the result is not a valid quoted value, an error is returned.
func Quote(value string, quoteType ast.StringAttributeType) (string, error) {
switch quoteType {
case ast.DoubleQuoted, ast.SingleQuoted:
var hasQuote bool
quote := quoteType.String()[0]
for i := 0; i < len(value); i++ {
if value[i] == quote && i > 1 && value[i-1] != '\\' {
hasQuote = true
break
}
}

for i, chr := range value {
if chr == '"' && i > 1 && value[i-1] != '\\' {
hasDoubleQuote = true
if hasQuote {
return "", errors.New("value %q contains unescaped quotes and can not be quoted without escaping")
}
if chr == '\'' && i > 1 && value[i-1] != '\\' {
hasSingleQuote = true
return quoteType.String() + value + quoteType.String(), nil

case ast.Bareword:
if !barewordRe.MatchString(value) {
return "", errors.New("value %q contains non bareword characters and can not be quoted as bareword without escaping")
}
return value, nil

default:
panic("quote type not supported")
}
}

// QuoteWithEscape returns a string with quotes for Logstash. Supported quote
// types are ast.DoubleQuoted, ast.SingleQuoted and ast.Bareword.
// The value will be escaped if necessary such, that the returned value is a
// valid quoted Logstash string.
// For ast.DoubleQuoted, all double quotes (`"`) are escaped to `\"`.
// For ast.SingleQuoted, all single quotes (`'`) are escaped to `\'`.
// For ast.Bareword, all characters not matching "[A-Za-z_][A-Za-z0-9_]+" are
// replaced with `_`.
func QuoteWithEscape(value string, quoteType ast.StringAttributeType) string {
switch quoteType {
case ast.DoubleQuoted:
if hasDoubleQuote && !escape {
return "", errors.New("value %q contains unescaped double quotes and can not be quoted with double quotes without escaping")
}
return `"` + escapeQuotes(value, '"') + `"`, nil
case ast.SingleQuoted:
if hasSingleQuote && !escape {
return "", errors.New("value %q contains unescaped single quotes and can not be quoted with double quotes without escaping")
}
return `'` + escapeQuotes(value, '\'') + `'`, nil
case ast.DoubleQuoted, ast.SingleQuoted:
quote := quoteType.String()[0]
return quoteType.String() + escapeQuotes(value, quote) + quoteType.String()

case ast.Bareword:
if !barewordRe.MatchString(value) && !escape {
return "", errors.New("value %q contains non bareword characters and can not be quoted as bareword without escaping")
}
return escapeBareword(value), nil
return escapeBareword(value)

default:
panic("quote type not supported")
}
Expand Down
127 changes: 92 additions & 35 deletions ast/astutil/quote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ func TestQuote(t *testing.T) {
name string
in string

want []string
wantErr []bool
wantEscaped []string
want []string
wantErr []bool
}{
{
name: "bareword",
Expand All @@ -30,11 +29,6 @@ func TestQuote(t *testing.T) {
ast.SingleQuoted: false,
ast.Bareword: false,
},
wantEscaped: []string{
ast.DoubleQuoted: `"bareword"`,
ast.SingleQuoted: `'bareword'`,
ast.Bareword: `bareword`,
},
},
{
name: "multiple words",
Expand All @@ -50,11 +44,6 @@ func TestQuote(t *testing.T) {
ast.SingleQuoted: false,
ast.Bareword: true,
},
wantEscaped: []string{
ast.DoubleQuoted: `"multiple words"`,
ast.SingleQuoted: `'multiple words'`,
ast.Bareword: `multiple_words`,
},
},
{
name: "double quote",
Expand All @@ -70,11 +59,6 @@ func TestQuote(t *testing.T) {
ast.SingleQuoted: false,
ast.Bareword: true,
},
wantEscaped: []string{
ast.DoubleQuoted: `"value with \" (double quote)"`,
ast.SingleQuoted: `'value with " (double quote)'`,
ast.Bareword: `value_with____double_quote_`,
},
},
{
name: "escaped double quote",
Expand All @@ -90,11 +74,6 @@ func TestQuote(t *testing.T) {
ast.SingleQuoted: false,
ast.Bareword: true,
},
wantEscaped: []string{
ast.DoubleQuoted: `"value with \" (escaped double quote)"`,
ast.SingleQuoted: `'value with \" (escaped double quote)'`,
ast.Bareword: `value_with_____escaped_double_quote_`,
},
},
{
name: "single quote",
Expand All @@ -110,11 +89,6 @@ func TestQuote(t *testing.T) {
ast.SingleQuoted: true,
ast.Bareword: true,
},
wantEscaped: []string{
ast.DoubleQuoted: `"value with ' (single quote)"`,
ast.SingleQuoted: `'value with \' (single quote)'`,
ast.Bareword: `value_with____single_quote_`,
},
},
{
name: "escaped single quote",
Expand All @@ -130,11 +104,6 @@ func TestQuote(t *testing.T) {
ast.SingleQuoted: false,
ast.Bareword: true,
},
wantEscaped: []string{
ast.DoubleQuoted: `"value with \' (escaped single quote)"`,
ast.SingleQuoted: `'value with \' (escaped single quote)'`,
ast.Bareword: `value_with_____escaped_single_quote_`,
},
},
}

Expand All @@ -147,18 +116,106 @@ func TestQuote(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
if len(tc.want) != 4 && len(tc.wantErr) != 4 {
t.Error("test case has an invalid number of want or wantErr values")
}
for name, quoteType := range quoteTypes {
t.Run(name, func(t *testing.T) {
got, err := astutil.Quote(tc.in, quoteType, false)
got, err := astutil.Quote(tc.in, quoteType)
if tc.wantErr[quoteType] != (err != nil) {
t.Errorf("wantErr %t, err: %v", tc.wantErr[quoteType], err)
}
if tc.want[quoteType] != got {
t.Errorf("want: %q, got: %q", tc.want[quoteType], got)
}
})
}
})
}
}

gotEscaped, _ := astutil.Quote(tc.in, quoteType, true)
func TestQuoteWithEscape(t *testing.T) {
tt := []struct {
name string
in string

wantEscaped []string
}{
{
name: "bareword",
in: `bareword`,

wantEscaped: []string{
ast.DoubleQuoted: `"bareword"`,
ast.SingleQuoted: `'bareword'`,
ast.Bareword: `bareword`,
},
},
{
name: "multiple words",
in: `multiple words`,

wantEscaped: []string{
ast.DoubleQuoted: `"multiple words"`,
ast.SingleQuoted: `'multiple words'`,
ast.Bareword: `multiple_words`,
},
},
{
name: "double quote",
in: `value with " (double quote)`,

wantEscaped: []string{
ast.DoubleQuoted: `"value with \" (double quote)"`,
ast.SingleQuoted: `'value with " (double quote)'`,
ast.Bareword: `value_with____double_quote_`,
},
},
{
name: "escaped double quote",
in: `value with \" (escaped double quote)`,

wantEscaped: []string{
ast.DoubleQuoted: `"value with \" (escaped double quote)"`,
ast.SingleQuoted: `'value with \" (escaped double quote)'`,
ast.Bareword: `value_with_____escaped_double_quote_`,
},
},
{
name: "single quote",
in: `value with ' (single quote)`,

wantEscaped: []string{
ast.DoubleQuoted: `"value with ' (single quote)"`,
ast.SingleQuoted: `'value with \' (single quote)'`,
ast.Bareword: `value_with____single_quote_`,
},
},
{
name: "escaped single quote",
in: `value with \' (escaped single quote)`,

wantEscaped: []string{
ast.DoubleQuoted: `"value with \' (escaped single quote)"`,
ast.SingleQuoted: `'value with \' (escaped single quote)'`,
ast.Bareword: `value_with_____escaped_single_quote_`,
},
},
}

quoteTypes := map[string]ast.StringAttributeType{
"double quote": ast.DoubleQuoted,
"single quote": ast.SingleQuoted,
"bareword": ast.Bareword,
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
if len(tc.wantEscaped) != 4 {
t.Error("test case has an invalid number of want values")
}
for name, quoteType := range quoteTypes {
t.Run(name, func(t *testing.T) {
gotEscaped := astutil.QuoteWithEscape(tc.in, quoteType)
if tc.wantEscaped[quoteType] != gotEscaped {
t.Errorf("want: %q, got: %q", tc.wantEscaped[quoteType], gotEscaped)
}
Expand Down

0 comments on commit 4605d71

Please sign in to comment.