Skip to content

Commit 4605d71

Browse files
authoredApr 17, 2021
Merge pull request #6 from breml/quote-string-fixed
Fix quote string
2 parents 29344d1 + c880cdd commit 4605d71

File tree

2 files changed

+132
-65
lines changed

2 files changed

+132
-65
lines changed
 

‎ast/astutil/quote.go

+40-30
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,54 @@ import (
1010

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

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

26-
for i, chr := range value {
27-
if chr == '"' && i > 1 && value[i-1] != '\\' {
28-
hasDoubleQuote = true
28+
if hasQuote {
29+
return "", errors.New("value %q contains unescaped quotes and can not be quoted without escaping")
2930
}
30-
if chr == '\'' && i > 1 && value[i-1] != '\\' {
31-
hasSingleQuote = true
31+
return quoteType.String() + value + quoteType.String(), nil
32+
33+
case ast.Bareword:
34+
if !barewordRe.MatchString(value) {
35+
return "", errors.New("value %q contains non bareword characters and can not be quoted as bareword without escaping")
3236
}
37+
return value, nil
38+
39+
default:
40+
panic("quote type not supported")
3341
}
42+
}
3443

44+
// QuoteWithEscape returns a string with quotes for Logstash. Supported quote
45+
// types are ast.DoubleQuoted, ast.SingleQuoted and ast.Bareword.
46+
// The value will be escaped if necessary such, that the returned value is a
47+
// valid quoted Logstash string.
48+
// For ast.DoubleQuoted, all double quotes (`"`) are escaped to `\"`.
49+
// For ast.SingleQuoted, all single quotes (`'`) are escaped to `\'`.
50+
// For ast.Bareword, all characters not matching "[A-Za-z_][A-Za-z0-9_]+" are
51+
// replaced with `_`.
52+
func QuoteWithEscape(value string, quoteType ast.StringAttributeType) string {
3553
switch quoteType {
36-
case ast.DoubleQuoted:
37-
if hasDoubleQuote && !escape {
38-
return "", errors.New("value %q contains unescaped double quotes and can not be quoted with double quotes without escaping")
39-
}
40-
return `"` + escapeQuotes(value, '"') + `"`, nil
41-
case ast.SingleQuoted:
42-
if hasSingleQuote && !escape {
43-
return "", errors.New("value %q contains unescaped single quotes and can not be quoted with double quotes without escaping")
44-
}
45-
return `'` + escapeQuotes(value, '\'') + `'`, nil
54+
case ast.DoubleQuoted, ast.SingleQuoted:
55+
quote := quoteType.String()[0]
56+
return quoteType.String() + escapeQuotes(value, quote) + quoteType.String()
57+
4658
case ast.Bareword:
47-
if !barewordRe.MatchString(value) && !escape {
48-
return "", errors.New("value %q contains non bareword characters and can not be quoted as bareword without escaping")
49-
}
50-
return escapeBareword(value), nil
59+
return escapeBareword(value)
60+
5161
default:
5262
panic("quote type not supported")
5363
}

‎ast/astutil/quote_test.go

+92-35
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ func TestQuote(t *testing.T) {
1212
name string
1313
in string
1414

15-
want []string
16-
wantErr []bool
17-
wantEscaped []string
15+
want []string
16+
wantErr []bool
1817
}{
1918
{
2019
name: "bareword",
@@ -30,11 +29,6 @@ func TestQuote(t *testing.T) {
3029
ast.SingleQuoted: false,
3130
ast.Bareword: false,
3231
},
33-
wantEscaped: []string{
34-
ast.DoubleQuoted: `"bareword"`,
35-
ast.SingleQuoted: `'bareword'`,
36-
ast.Bareword: `bareword`,
37-
},
3832
},
3933
{
4034
name: "multiple words",
@@ -50,11 +44,6 @@ func TestQuote(t *testing.T) {
5044
ast.SingleQuoted: false,
5145
ast.Bareword: true,
5246
},
53-
wantEscaped: []string{
54-
ast.DoubleQuoted: `"multiple words"`,
55-
ast.SingleQuoted: `'multiple words'`,
56-
ast.Bareword: `multiple_words`,
57-
},
5847
},
5948
{
6049
name: "double quote",
@@ -70,11 +59,6 @@ func TestQuote(t *testing.T) {
7059
ast.SingleQuoted: false,
7160
ast.Bareword: true,
7261
},
73-
wantEscaped: []string{
74-
ast.DoubleQuoted: `"value with \" (double quote)"`,
75-
ast.SingleQuoted: `'value with " (double quote)'`,
76-
ast.Bareword: `value_with____double_quote_`,
77-
},
7862
},
7963
{
8064
name: "escaped double quote",
@@ -90,11 +74,6 @@ func TestQuote(t *testing.T) {
9074
ast.SingleQuoted: false,
9175
ast.Bareword: true,
9276
},
93-
wantEscaped: []string{
94-
ast.DoubleQuoted: `"value with \" (escaped double quote)"`,
95-
ast.SingleQuoted: `'value with \" (escaped double quote)'`,
96-
ast.Bareword: `value_with_____escaped_double_quote_`,
97-
},
9877
},
9978
{
10079
name: "single quote",
@@ -110,11 +89,6 @@ func TestQuote(t *testing.T) {
11089
ast.SingleQuoted: true,
11190
ast.Bareword: true,
11291
},
113-
wantEscaped: []string{
114-
ast.DoubleQuoted: `"value with ' (single quote)"`,
115-
ast.SingleQuoted: `'value with \' (single quote)'`,
116-
ast.Bareword: `value_with____single_quote_`,
117-
},
11892
},
11993
{
12094
name: "escaped single quote",
@@ -130,11 +104,6 @@ func TestQuote(t *testing.T) {
130104
ast.SingleQuoted: false,
131105
ast.Bareword: true,
132106
},
133-
wantEscaped: []string{
134-
ast.DoubleQuoted: `"value with \' (escaped single quote)"`,
135-
ast.SingleQuoted: `'value with \' (escaped single quote)'`,
136-
ast.Bareword: `value_with_____escaped_single_quote_`,
137-
},
138107
},
139108
}
140109

@@ -147,18 +116,106 @@ func TestQuote(t *testing.T) {
147116
for _, tc := range tt {
148117
t.Run(tc.name, func(t *testing.T) {
149118
if len(tc.want) != 4 && len(tc.wantErr) != 4 {
119+
t.Error("test case has an invalid number of want or wantErr values")
150120
}
151121
for name, quoteType := range quoteTypes {
152122
t.Run(name, func(t *testing.T) {
153-
got, err := astutil.Quote(tc.in, quoteType, false)
123+
got, err := astutil.Quote(tc.in, quoteType)
154124
if tc.wantErr[quoteType] != (err != nil) {
155125
t.Errorf("wantErr %t, err: %v", tc.wantErr[quoteType], err)
156126
}
157127
if tc.want[quoteType] != got {
158128
t.Errorf("want: %q, got: %q", tc.want[quoteType], got)
159129
}
130+
})
131+
}
132+
})
133+
}
134+
}
160135

161-
gotEscaped, _ := astutil.Quote(tc.in, quoteType, true)
136+
func TestQuoteWithEscape(t *testing.T) {
137+
tt := []struct {
138+
name string
139+
in string
140+
141+
wantEscaped []string
142+
}{
143+
{
144+
name: "bareword",
145+
in: `bareword`,
146+
147+
wantEscaped: []string{
148+
ast.DoubleQuoted: `"bareword"`,
149+
ast.SingleQuoted: `'bareword'`,
150+
ast.Bareword: `bareword`,
151+
},
152+
},
153+
{
154+
name: "multiple words",
155+
in: `multiple words`,
156+
157+
wantEscaped: []string{
158+
ast.DoubleQuoted: `"multiple words"`,
159+
ast.SingleQuoted: `'multiple words'`,
160+
ast.Bareword: `multiple_words`,
161+
},
162+
},
163+
{
164+
name: "double quote",
165+
in: `value with " (double quote)`,
166+
167+
wantEscaped: []string{
168+
ast.DoubleQuoted: `"value with \" (double quote)"`,
169+
ast.SingleQuoted: `'value with " (double quote)'`,
170+
ast.Bareword: `value_with____double_quote_`,
171+
},
172+
},
173+
{
174+
name: "escaped double quote",
175+
in: `value with \" (escaped double quote)`,
176+
177+
wantEscaped: []string{
178+
ast.DoubleQuoted: `"value with \" (escaped double quote)"`,
179+
ast.SingleQuoted: `'value with \" (escaped double quote)'`,
180+
ast.Bareword: `value_with_____escaped_double_quote_`,
181+
},
182+
},
183+
{
184+
name: "single quote",
185+
in: `value with ' (single quote)`,
186+
187+
wantEscaped: []string{
188+
ast.DoubleQuoted: `"value with ' (single quote)"`,
189+
ast.SingleQuoted: `'value with \' (single quote)'`,
190+
ast.Bareword: `value_with____single_quote_`,
191+
},
192+
},
193+
{
194+
name: "escaped single quote",
195+
in: `value with \' (escaped single quote)`,
196+
197+
wantEscaped: []string{
198+
ast.DoubleQuoted: `"value with \' (escaped single quote)"`,
199+
ast.SingleQuoted: `'value with \' (escaped single quote)'`,
200+
ast.Bareword: `value_with_____escaped_single_quote_`,
201+
},
202+
},
203+
}
204+
205+
quoteTypes := map[string]ast.StringAttributeType{
206+
"double quote": ast.DoubleQuoted,
207+
"single quote": ast.SingleQuoted,
208+
"bareword": ast.Bareword,
209+
}
210+
211+
for _, tc := range tt {
212+
t.Run(tc.name, func(t *testing.T) {
213+
if len(tc.wantEscaped) != 4 {
214+
t.Error("test case has an invalid number of want values")
215+
}
216+
for name, quoteType := range quoteTypes {
217+
t.Run(name, func(t *testing.T) {
218+
gotEscaped := astutil.QuoteWithEscape(tc.in, quoteType)
162219
if tc.wantEscaped[quoteType] != gotEscaped {
163220
t.Errorf("want: %q, got: %q", tc.wantEscaped[quoteType], gotEscaped)
164221
}

0 commit comments

Comments
 (0)