Skip to content

Commit 41d991e

Browse files
author
Jay Conrod
committed
cmd/internal/str: add utilities for quoting and splitting args
JoinAndQuoteFields does the inverse of SplitQuotedFields: it joins a list of arguments with spaces into one string, quoting arguments that contain spaces or quotes. QuotedStringListFlag uses SplitQuotedFields and JoinAndQuoteFields together to define new flags that accept lists of arguments. For #41400 Change-Id: I4986b753cb5e6fabb5b489bf26aedab889f853f5 Reviewed-on: https://go-review.googlesource.com/c/go/+/334731 Trust: Jay Conrod <[email protected]> Trust: Michael Matloob <[email protected]> Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]> Reviewed-by: Michael Matloob <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/341935
1 parent 4466141 commit 41d991e

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

src/cmd/internal/str/str.go

+72
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package str
77

88
import (
99
"bytes"
10+
"flag"
1011
"fmt"
12+
"strings"
1113
"unicode"
1214
"unicode/utf8"
1315
)
@@ -153,3 +155,73 @@ func SplitQuotedFields(s string) ([]string, error) {
153155
}
154156
return f, nil
155157
}
158+
159+
// JoinAndQuoteFields joins a list of arguments into a string that can be parsed
160+
// with SplitQuotedFields. Arguments are quoted only if necessary; arguments
161+
// without spaces or quotes are kept as-is. No argument may contain both
162+
// single and double quotes.
163+
func JoinAndQuoteFields(args []string) (string, error) {
164+
var buf []byte
165+
for i, arg := range args {
166+
if i > 0 {
167+
buf = append(buf, ' ')
168+
}
169+
var sawSpace, sawSingleQuote, sawDoubleQuote bool
170+
for _, c := range arg {
171+
switch {
172+
case c > unicode.MaxASCII:
173+
continue
174+
case isSpaceByte(byte(c)):
175+
sawSpace = true
176+
case c == '\'':
177+
sawSingleQuote = true
178+
case c == '"':
179+
sawDoubleQuote = true
180+
}
181+
}
182+
switch {
183+
case !sawSpace && !sawSingleQuote && !sawDoubleQuote:
184+
buf = append(buf, []byte(arg)...)
185+
186+
case !sawSingleQuote:
187+
buf = append(buf, '\'')
188+
buf = append(buf, []byte(arg)...)
189+
buf = append(buf, '\'')
190+
191+
case !sawDoubleQuote:
192+
buf = append(buf, '"')
193+
buf = append(buf, []byte(arg)...)
194+
buf = append(buf, '"')
195+
196+
default:
197+
return "", fmt.Errorf("argument %q contains both single and double quotes and cannot be quoted", arg)
198+
}
199+
}
200+
return string(buf), nil
201+
}
202+
203+
// A QuotedStringListFlag parses a list of string arguments encoded with
204+
// JoinAndQuoteFields. It is useful for flags like cmd/link's -extldflags.
205+
type QuotedStringListFlag []string
206+
207+
var _ flag.Value = (*QuotedStringListFlag)(nil)
208+
209+
func (f *QuotedStringListFlag) Set(v string) error {
210+
fs, err := SplitQuotedFields(v)
211+
if err != nil {
212+
return err
213+
}
214+
*f = fs[:len(fs):len(fs)]
215+
return nil
216+
}
217+
218+
func (f *QuotedStringListFlag) String() string {
219+
if f == nil {
220+
return ""
221+
}
222+
s, err := JoinAndQuoteFields(*f)
223+
if err != nil {
224+
return strings.Join(*f, " ")
225+
}
226+
return s
227+
}

src/cmd/internal/str/str_test.go

+82-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
package str
66

7-
import "testing"
7+
import (
8+
"reflect"
9+
"strings"
10+
"testing"
11+
)
812

913
var foldDupTests = []struct {
1014
list []string
@@ -25,3 +29,80 @@ func TestFoldDup(t *testing.T) {
2529
}
2630
}
2731
}
32+
33+
func TestSplitQuotedFields(t *testing.T) {
34+
for _, test := range []struct {
35+
name string
36+
value string
37+
want []string
38+
wantErr string
39+
}{
40+
{name: "empty", value: "", want: nil},
41+
{name: "space", value: " ", want: nil},
42+
{name: "one", value: "a", want: []string{"a"}},
43+
{name: "leading_space", value: " a", want: []string{"a"}},
44+
{name: "trailing_space", value: "a ", want: []string{"a"}},
45+
{name: "two", value: "a b", want: []string{"a", "b"}},
46+
{name: "two_multi_space", value: "a b", want: []string{"a", "b"}},
47+
{name: "two_tab", value: "a\tb", want: []string{"a", "b"}},
48+
{name: "two_newline", value: "a\nb", want: []string{"a", "b"}},
49+
{name: "quote_single", value: `'a b'`, want: []string{"a b"}},
50+
{name: "quote_double", value: `"a b"`, want: []string{"a b"}},
51+
{name: "quote_both", value: `'a '"b "`, want: []string{"a ", "b "}},
52+
{name: "quote_contains", value: `'a "'"'b"`, want: []string{`a "`, `'b`}},
53+
{name: "escape", value: `\'`, want: []string{`\'`}},
54+
{name: "quote_unclosed", value: `'a`, wantErr: "unterminated ' string"},
55+
} {
56+
t.Run(test.name, func(t *testing.T) {
57+
got, err := SplitQuotedFields(test.value)
58+
if err != nil {
59+
if test.wantErr == "" {
60+
t.Fatalf("unexpected error: %v", err)
61+
} else if errMsg := err.Error(); !strings.Contains(errMsg, test.wantErr) {
62+
t.Fatalf("error %q does not contain %q", errMsg, test.wantErr)
63+
}
64+
return
65+
}
66+
if test.wantErr != "" {
67+
t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
68+
}
69+
if !reflect.DeepEqual(got, test.want) {
70+
t.Errorf("got %q; want %q", got, test.want)
71+
}
72+
})
73+
}
74+
}
75+
76+
func TestJoinAndQuoteFields(t *testing.T) {
77+
for _, test := range []struct {
78+
name string
79+
args []string
80+
want, wantErr string
81+
}{
82+
{name: "empty", args: nil, want: ""},
83+
{name: "one", args: []string{"a"}, want: "a"},
84+
{name: "two", args: []string{"a", "b"}, want: "a b"},
85+
{name: "space", args: []string{"a ", "b"}, want: "'a ' b"},
86+
{name: "newline", args: []string{"a\n", "b"}, want: "'a\n' b"},
87+
{name: "quote", args: []string{`'a `, "b"}, want: `"'a " b`},
88+
{name: "unquoteable", args: []string{`'"`}, wantErr: "contains both single and double quotes and cannot be quoted"},
89+
} {
90+
t.Run(test.name, func(t *testing.T) {
91+
got, err := JoinAndQuoteFields(test.args)
92+
if err != nil {
93+
if test.wantErr == "" {
94+
t.Fatalf("unexpected error: %v", err)
95+
} else if errMsg := err.Error(); !strings.Contains(errMsg, test.wantErr) {
96+
t.Fatalf("error %q does not contain %q", errMsg, test.wantErr)
97+
}
98+
return
99+
}
100+
if test.wantErr != "" {
101+
t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
102+
}
103+
if got != test.want {
104+
t.Errorf("got %s; want %s", got, test.want)
105+
}
106+
})
107+
}
108+
}

0 commit comments

Comments
 (0)