blob: c6ef7328cb8dcb130187798937ed77898b071661 [file] [log] [blame]
package shell
import (
"fmt"
"io"
"log"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestQuote(t *testing.T) {
type testCase struct{ in, want string }
tests := []testCase{
{"", "''"}, // empty is special
{"abc", "abc"}, // nothing to quote
{"--flag", "--flag"}, // "
{"'abc", `\'abc`}, // single quote only
{"abc'", `abc\'`}, // "
{`shan't`, `shan\'t`}, // "
{"--flag=value", `'--flag=value'`},
{"a b\tc", "'a b\tc'"},
{`a"b"c`, `'a"b"c'`},
{`'''`, `\'\'\'`},
{`\`, `'\'`},
{`'a=b`, `\''a=b'`}, // quotes and other stuff
{`a='b`, `'a='\''b'`}, // "
{`a=b'`, `'a=b'\'`}, // "
}
// Verify that all the designated special characters get quoted.
for _, c := range shouldQuote + mustQuote {
tests = append(tests, testCase{
in: string(c),
want: fmt.Sprintf(`'%c'`, c),
})
}
for _, test := range tests {
got := Quote(test.in)
if got != test.want {
t.Errorf("Quote %q: got %q, want %q", test.in, got, test.want)
}
}
}
func TestSplit(t *testing.T) {
tests := []struct {
in string
want []string
ok bool
}{
// Variations of empty input yield an empty split.
{"", nil, true},
{" ", nil, true},
{"\t", nil, true},
{"\n ", nil, true},
// Various escape sequences work properly.
{`\ `, []string{" "}, true},
{`a\ `, []string{"a "}, true},
{`\\a`, []string{`\a`}, true},
{`"a\"b"`, []string{`a"b`}, true},
{`'\'`, []string{"\\"}, true},
// Leading and trailing whitespace are discarded correctly.
{"a", []string{"a"}, true},
{" a", []string{"a"}, true},
{"a\n", []string{"a"}, true},
// Escaped newlines are magic in the correct ways.
{"a\\\nb", []string{"ab"}, true},
{"a \\\n b\tc", []string{"a", "b", "c"}, true},
// Various splits with and without quotes. Quoted whitespace is
// preserved.
{"a b c", []string{"a", "b", "c"}, true},
{`a 'b c'`, []string{"a", "b c"}, true},
{"\"a\nb\"cd e'f'", []string{"a\nbcd", "ef"}, true},
{"'\n \t '", []string{"\n \t "}, true},
// Quoted empty strings are preserved in various places.
{"''", []string{""}, true},
{"a ''", []string{"a", ""}, true},
{" a \"\" b ", []string{"a", "", "b"}, true},
{"'' a", []string{"", "a"}, true},
// Unbalanced quotation marks and escapes are detected.
{"\\", []string{""}, false}, // escape without a target
{"'", []string{""}, false}, // unclosed single
{`"`, []string{""}, false}, // unclosed double
{`'\''`, []string{`\`}, false}, // unclosed connected double
{`"\\" '`, []string{`\`, ``}, false}, // unclosed separate single
{"a 'b c", []string{"a", "b c"}, false},
{`a "b c`, []string{"a", "b c"}, false},
{`a "b \"`, []string{"a", `b "`}, false},
}
for _, test := range tests {
got, ok := Split(test.in)
if ok != test.ok {
t.Errorf("Split %#q: got valid=%v, want %v", test.in, ok, test.ok)
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Split %#q: (-want, +got)\n%s", test.in, diff)
}
}
}
func TestScannerSplit(t *testing.T) {
tests := []struct {
in string
want, rest []string
}{
{"", nil, nil},
{" ", nil, nil},
{"--", nil, nil},
{"a -- b", []string{"a"}, []string{"b"}},
{"a b c -- d -- e ", []string{"a", "b", "c"}, []string{"d", "--", "e"}},
{`"a b c --" -- "d "`, []string{"a b c --"}, []string{"d "}},
{` -- "foo`, nil, []string{"foo"}}, // unterminated
{"cmd -flag -- arg1 arg2", []string{"cmd", "-flag"}, []string{"arg1", "arg2"}},
}
for _, test := range tests {
t.Logf("Scanner split input: %q", test.in)
s := NewScanner(strings.NewReader(test.in))
var got, rest []string
for s.Next() {
if s.Text() == "--" {
rest = s.Split()
break
}
got = append(got, s.Text())
}
if s.Err() != io.EOF {
t.Errorf("Unexpected scan error: %v", s.Err())
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Scanner split prefix: (-want, +got)\n%s", diff)
}
if diff := cmp.Diff(test.rest, rest); diff != "" {
t.Errorf("Scanner split suffix: (-want, +got)\n%s", diff)
}
}
}
func TestRoundTrip(t *testing.T) {
tests := [][]string{
nil,
{"a"},
{"a "},
{"a", "b", "c"},
{"a", "b c"},
{"--flag=value"},
{"m='$USER'", "nop+", "$$"},
{`"a" b `, "c"},
{"odd's", "bodkins", "x'", "x''", "x\"\"", "$x':y"},
{"a=b", "--foo", "${bar}", `\$`},
{"cat", "a${b}.txt", "|", "tee", "capture", "2>", "/dev/null"},
}
for _, test := range tests {
s := Join(test)
t.Logf("Join %#q = %v", test, s)
got, ok := Split(s)
if !ok {
t.Errorf("Split %+q: should be valid, but is not", s)
}
if diff := cmp.Diff(test, got); diff != "" {
t.Errorf("Split %+q: (-want, +got)\n%s", s, diff)
}
}
}
func ExampleScanner() {
const input = `a "free range" exploration of soi\ disant novelties`
s := NewScanner(strings.NewReader(input))
sum, count := 0, 0
for s.Next() {
count++
sum += len(s.Text())
}
fmt.Println(len(input), count, sum, s.Complete(), s.Err())
// Output: 51 6 43 true EOF
}
func ExampleScanner_Rest() {
const input = `things 'and stuff' %end% all the remaining stuff`
s := NewScanner(strings.NewReader(input))
for s.Next() {
if s.Text() == "%end%" {
fmt.Print("found marker; ")
break
}
}
rest, err := io.ReadAll(s.Rest())
if err != nil {
log.Fatal(err)
}
fmt.Println(string(rest))
// Output: found marker; all the remaining stuff
}
func ExampleScanner_Each() {
const input = `a\ b 'c d' "e f's g" stop "go directly to jail"`
if err := NewScanner(strings.NewReader(input)).Each(func(tok string) bool {
fmt.Println(tok)
return tok != "stop"
}); err != nil {
log.Fatal(err)
}
// Output:
// a b
// c d
// e f's g
// stop
}
func ExampleScanner_Split() {
const input = `cmd -flag=t -- foo bar baz`
s := NewScanner(strings.NewReader(input))
for s.Next() {
if s.Text() == "--" {
fmt.Println("** Args:", strings.Join(s.Split(), ", "))
} else {
fmt.Println(s.Text())
}
}
// Output:
// cmd
// -flag=t
// ** Args: foo, bar, baz
}