| 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 |
| } |