mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
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 golang/go#41400 Change-Id: I4986b753cb5e6fabb5b489bf26aedab889f853f5 Reviewed-on: https://go-review.googlesource.com/c/go/+/334731 Trust: Jay Conrod <jayconrod@google.com> Trust: Michael Matloob <matloob@golang.org> Run-TryBot: Jay Conrod <jayconrod@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Bryan C. Mills <bcmills@google.com> Reviewed-by: Michael Matloob <matloob@golang.org> Reviewed-on: https://go-review.googlesource.com/c/go/+/341935
This commit is contained in:
parent
4466141822
commit
41d991e4e1
2 changed files with 154 additions and 1 deletions
|
|
@ -7,7 +7,9 @@ package str
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
|
@ -153,3 +155,73 @@ func SplitQuotedFields(s string) ([]string, error) {
|
|||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// JoinAndQuoteFields joins a list of arguments into a string that can be parsed
|
||||
// with SplitQuotedFields. Arguments are quoted only if necessary; arguments
|
||||
// without spaces or quotes are kept as-is. No argument may contain both
|
||||
// single and double quotes.
|
||||
func JoinAndQuoteFields(args []string) (string, error) {
|
||||
var buf []byte
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
buf = append(buf, ' ')
|
||||
}
|
||||
var sawSpace, sawSingleQuote, sawDoubleQuote bool
|
||||
for _, c := range arg {
|
||||
switch {
|
||||
case c > unicode.MaxASCII:
|
||||
continue
|
||||
case isSpaceByte(byte(c)):
|
||||
sawSpace = true
|
||||
case c == '\'':
|
||||
sawSingleQuote = true
|
||||
case c == '"':
|
||||
sawDoubleQuote = true
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case !sawSpace && !sawSingleQuote && !sawDoubleQuote:
|
||||
buf = append(buf, []byte(arg)...)
|
||||
|
||||
case !sawSingleQuote:
|
||||
buf = append(buf, '\'')
|
||||
buf = append(buf, []byte(arg)...)
|
||||
buf = append(buf, '\'')
|
||||
|
||||
case !sawDoubleQuote:
|
||||
buf = append(buf, '"')
|
||||
buf = append(buf, []byte(arg)...)
|
||||
buf = append(buf, '"')
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("argument %q contains both single and double quotes and cannot be quoted", arg)
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// A QuotedStringListFlag parses a list of string arguments encoded with
|
||||
// JoinAndQuoteFields. It is useful for flags like cmd/link's -extldflags.
|
||||
type QuotedStringListFlag []string
|
||||
|
||||
var _ flag.Value = (*QuotedStringListFlag)(nil)
|
||||
|
||||
func (f *QuotedStringListFlag) Set(v string) error {
|
||||
fs, err := SplitQuotedFields(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = fs[:len(fs):len(fs)]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *QuotedStringListFlag) String() string {
|
||||
if f == nil {
|
||||
return ""
|
||||
}
|
||||
s, err := JoinAndQuoteFields(*f)
|
||||
if err != nil {
|
||||
return strings.Join(*f, " ")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@
|
|||
|
||||
package str
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var foldDupTests = []struct {
|
||||
list []string
|
||||
|
|
@ -25,3 +29,80 @@ func TestFoldDup(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitQuotedFields(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value string
|
||||
want []string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "empty", value: "", want: nil},
|
||||
{name: "space", value: " ", want: nil},
|
||||
{name: "one", value: "a", want: []string{"a"}},
|
||||
{name: "leading_space", value: " a", want: []string{"a"}},
|
||||
{name: "trailing_space", value: "a ", want: []string{"a"}},
|
||||
{name: "two", value: "a b", want: []string{"a", "b"}},
|
||||
{name: "two_multi_space", value: "a b", want: []string{"a", "b"}},
|
||||
{name: "two_tab", value: "a\tb", want: []string{"a", "b"}},
|
||||
{name: "two_newline", value: "a\nb", want: []string{"a", "b"}},
|
||||
{name: "quote_single", value: `'a b'`, want: []string{"a b"}},
|
||||
{name: "quote_double", value: `"a b"`, want: []string{"a b"}},
|
||||
{name: "quote_both", value: `'a '"b "`, want: []string{"a ", "b "}},
|
||||
{name: "quote_contains", value: `'a "'"'b"`, want: []string{`a "`, `'b`}},
|
||||
{name: "escape", value: `\'`, want: []string{`\'`}},
|
||||
{name: "quote_unclosed", value: `'a`, wantErr: "unterminated ' string"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := SplitQuotedFields(test.value)
|
||||
if err != nil {
|
||||
if test.wantErr == "" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else if errMsg := err.Error(); !strings.Contains(errMsg, test.wantErr) {
|
||||
t.Fatalf("error %q does not contain %q", errMsg, test.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if test.wantErr != "" {
|
||||
t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("got %q; want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinAndQuoteFields(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want, wantErr string
|
||||
}{
|
||||
{name: "empty", args: nil, want: ""},
|
||||
{name: "one", args: []string{"a"}, want: "a"},
|
||||
{name: "two", args: []string{"a", "b"}, want: "a b"},
|
||||
{name: "space", args: []string{"a ", "b"}, want: "'a ' b"},
|
||||
{name: "newline", args: []string{"a\n", "b"}, want: "'a\n' b"},
|
||||
{name: "quote", args: []string{`'a `, "b"}, want: `"'a " b`},
|
||||
{name: "unquoteable", args: []string{`'"`}, wantErr: "contains both single and double quotes and cannot be quoted"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := JoinAndQuoteFields(test.args)
|
||||
if err != nil {
|
||||
if test.wantErr == "" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else if errMsg := err.Error(); !strings.Contains(errMsg, test.wantErr) {
|
||||
t.Fatalf("error %q does not contain %q", errMsg, test.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if test.wantErr != "" {
|
||||
t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Errorf("got %s; want %s", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue