github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/parse/parse_test.go (about) 1 package parse 2 3 import ( 4 "fmt" 5 "os" 6 "testing" 7 ) 8 9 func a(c ...interface{}) ast { 10 // Shorthand used for checking Compound and levels beneath. 11 return ast{"Chunk/Pipeline/Form", fs{"Head": "a", "Args": c}} 12 } 13 14 var testCases = []struct { 15 name string 16 code string 17 node Node 18 want ast 19 20 wantErrPart string 21 wantErrAtEnd bool 22 wantErrMsg string 23 }{ 24 // Chunk 25 { 26 name: "empty chunk", 27 code: "", 28 node: &Chunk{}, 29 want: ast{"Chunk", fs{"Pipelines": nil}}, 30 }, 31 { 32 name: "multiple pipelines separated by newlines and semicolons", 33 code: "a;b;c\n;d", 34 node: &Chunk{}, 35 want: ast{"Chunk", fs{"Pipelines": []string{"a", "b", "c", "d"}}}, 36 }, 37 { 38 name: "extra newlines and semicolons do not result in empty pipelines", 39 code: " ;\n\n ls \t ;\n", 40 node: &Chunk{}, 41 want: ast{"Chunk", fs{"Pipelines": []string{"ls \t "}}}, 42 }, 43 44 // Pipeline 45 { 46 name: "pipeline", 47 code: "a|b|c|d", 48 node: &Pipeline{}, 49 want: ast{"Pipeline", fs{"Forms": []string{"a", "b", "c", "d"}}}, 50 }, 51 { 52 name: "newlines after pipes are allowed", 53 code: "a| \n \n b", 54 node: &Pipeline{}, 55 want: ast{"Pipeline", fs{"Forms": []string{"a", "b"}}}, 56 }, 57 58 { 59 name: "no form after pipe", 60 code: "a|", 61 node: &Chunk{}, 62 wantErrAtEnd: true, 63 wantErrMsg: "should be form", 64 }, 65 66 // Form 67 { 68 name: "command form", 69 code: "ls x y", 70 node: &Form{}, 71 want: ast{"Form", fs{ 72 "Head": "ls", 73 "Args": []string{"x", "y"}}}, 74 }, 75 { 76 name: "assignment form", 77 code: "k=v k[a][b]=v {a,b[1]}=(ha)", 78 node: &Form{}, 79 want: ast{"Form", fs{ 80 "Assignments": []string{"k=v", "k[a][b]=v", "{a,b[1]}=(ha)"}}}, 81 }, 82 { 83 name: "temporary assignment", 84 code: "k=v k[a][b]=v a", 85 node: &Form{}, 86 want: ast{"Form", fs{ 87 "Assignments": []string{"k=v", "k[a][b]=v"}, 88 "Head": "a"}}, 89 }, 90 { 91 name: "redirection", 92 code: "a >b", 93 node: &Form{}, 94 want: ast{"Form", fs{ 95 "Head": "a", 96 "Redirs": []ast{ 97 {"Redir", fs{"Mode": Write, "Right": "b"}}}, 98 }}, 99 }, 100 { 101 name: "advanced redirections", 102 code: "a >>b 2>b 3>&- 4>&1 5<c 6<>d", 103 node: &Form{}, 104 want: ast{"Form", fs{ 105 "Head": "a", 106 "Redirs": []ast{ 107 {"Redir", fs{"Mode": Append, "Right": "b"}}, 108 {"Redir", fs{"Left": "2", "Mode": Write, "Right": "b"}}, 109 {"Redir", fs{"Left": "3", "Mode": Write, "RightIsFd": true, "Right": "-"}}, 110 {"Redir", fs{"Left": "4", "Mode": Write, "RightIsFd": true, "Right": "1"}}, 111 {"Redir", fs{"Left": "5", "Mode": Read, "Right": "c"}}, 112 {"Redir", fs{"Left": "6", "Mode": ReadWrite, "Right": "d"}}, 113 }, 114 }}}, 115 { 116 name: "command options", 117 code: "a &a=1 x &b=2", 118 node: &Form{}, 119 want: ast{"Form", fs{ 120 "Head": "a", 121 "Args": []string{"x"}, 122 "Opts": []string{"&a=1", "&b=2"}, 123 }}, 124 // More tests for MapPair below with map syntax 125 }, 126 127 { 128 name: "bogus ampersand in command form", 129 code: "a & &", 130 node: &Chunk{}, 131 wantErrPart: "&", 132 wantErrMsg: "unexpected rune '&'", 133 }, 134 { 135 name: "no filename redirection source", 136 code: "a >", 137 node: &Chunk{}, 138 wantErrAtEnd: true, 139 wantErrMsg: "should be a composite term representing filename", 140 }, 141 { 142 name: "no FD direction source", 143 code: "a >&", 144 node: &Chunk{}, 145 wantErrAtEnd: true, 146 wantErrMsg: "should be a composite term representing fd", 147 }, 148 149 // Filter 150 { 151 name: "empty filter", 152 code: "", 153 node: &Filter{}, 154 want: ast{"Filter", fs{}}, 155 }, 156 { 157 name: "filter with arguments", 158 code: "foo bar", 159 node: &Filter{}, 160 want: ast{"Filter", fs{"Args": []string{"foo", "bar"}}}, 161 }, 162 { 163 name: "filter with options", 164 code: "&foo=bar &lorem=ipsum", 165 node: &Filter{}, 166 want: ast{"Filter", fs{"Opts": []string{"&foo=bar", "&lorem=ipsum"}}}, 167 }, 168 { 169 name: "filter mixing arguments and options", 170 code: "foo &a=b bar &x=y", 171 node: &Filter{}, 172 want: ast{"Filter", fs{ 173 "Args": []string{"foo", "bar"}, 174 "Opts": []string{"&a=b", "&x=y"}}}, 175 }, 176 { 177 name: "filter with leading and trailing whitespaces", 178 code: " foo ", 179 node: &Filter{}, 180 want: ast{"Filter", fs{"Args": []string{"foo"}}}, 181 }, 182 183 // Compound 184 { 185 name: "compound expression", 186 code: `b"foo"?$c*'xyz'`, 187 node: &Compound{}, 188 want: ast{"Compound", fs{ 189 "Indexings": []string{"b", `"foo"`, "?", "$c", "*", "'xyz'"}}}, 190 }, 191 192 // Indexing 193 { 194 name: "indexing expression", 195 code: "$b[c][d][\ne\n]", 196 node: &Indexing{}, 197 want: ast{"Indexing", fs{ 198 "Head": "$b", "Indices": []string{"c", "d", "\ne\n"}}}, 199 }, 200 201 // Primary 202 { 203 name: "bareword", 204 code: "foo", 205 node: &Primary{}, 206 want: ast{"Primary", fs{"Type": Bareword, "Value": "foo"}}, 207 }, 208 { 209 name: "bareword with all allowed symbols", 210 code: "./\\@%+!=,", 211 node: &Primary{}, 212 want: ast{"Primary", fs{"Type": Bareword, "Value": "./\\@%+!=,"}}, 213 }, 214 { 215 name: "single-quoted string", 216 code: "'''x''y'''", 217 node: &Primary{}, 218 want: ast{"Primary", fs{"Type": SingleQuoted, "Value": "'x'y'"}}, 219 }, 220 { 221 name: "double-quoted string with control char escape sequences", 222 code: `"[\c?\c@\cI\^I\^[]"`, 223 node: &Primary{}, 224 want: ast{"Primary", fs{ 225 "Type": DoubleQuoted, 226 "Value": "[\x7f\x00\t\t\x1b]", 227 }}, 228 }, 229 { 230 name: "double-quoted string with single-char escape sequences", 231 code: `"[\n\t\a\v\\\"]"`, 232 node: &Primary{}, 233 want: ast{"Primary", fs{ 234 "Type": DoubleQuoted, 235 "Value": "[\n\t\a\v\\\"]", 236 }}, 237 }, 238 { 239 name: "double-quoted string with numerical escape sequences for codepoints", 240 code: `"b\^[\u548c\U0002CE23\n\t\\"`, 241 node: &Primary{}, 242 want: ast{"Primary", fs{ 243 "Type": DoubleQuoted, 244 "Value": "b\x1b\u548c\U0002CE23\n\t\\", 245 }}, 246 }, 247 { 248 name: "double-quoted string with numerical escape sequences for bytes", 249 code: `"\123\321 \x7f\xff"`, 250 node: &Primary{}, 251 want: ast{"Primary", fs{ 252 "Type": DoubleQuoted, 253 "Value": "\123\321 \x7f\xff", 254 }}, 255 }, 256 { 257 name: "wildcard", 258 code: "a * ? ** ??", 259 node: &Chunk{}, 260 want: a( 261 ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "*"}}, 262 ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "?"}}, 263 ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "**"}}, 264 ast{"Compound", fs{"Indexings": []string{"?", "?"}}}, 265 ), 266 }, 267 { 268 name: "variable", 269 code: `a $x $'!@#' $"\n"`, 270 node: &Chunk{}, 271 want: a( 272 ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "x"}}, 273 ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "!@#"}}, 274 ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "\n"}}, 275 ), 276 }, 277 { 278 name: "list", 279 code: "a [] [ ] [1] [ 2] [3 ] [\n 4 \n5\n 6 7 \n]", 280 node: &Chunk{}, 281 want: a( 282 ast{"Compound/Indexing/Primary", fs{ 283 "Type": List, 284 "Elements": []ast{}}}, 285 ast{"Compound/Indexing/Primary", fs{ 286 "Type": List, 287 "Elements": []ast{}}}, 288 ast{"Compound/Indexing/Primary", fs{ 289 "Type": List, 290 "Elements": []string{"1"}}}, 291 ast{"Compound/Indexing/Primary", fs{ 292 "Type": List, 293 "Elements": []string{"2"}}}, 294 ast{"Compound/Indexing/Primary", fs{ 295 "Type": List, 296 "Elements": []string{"3"}}}, 297 ast{"Compound/Indexing/Primary", fs{ 298 "Type": List, 299 "Elements": []string{"4", "5", "6", "7"}}}, 300 ), 301 }, 302 { 303 name: "map", 304 code: "a [&k=v] [ &k=v] [&k=v ] [ &k=v ] [ &k= v] [&k= \n v] [\n&a=b &c=d \n &e=f\n\n]", 305 node: &Chunk{}, 306 want: a( 307 ast{"Compound/Indexing/Primary", fs{ 308 "Type": Map, 309 "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, 310 ast{"Compound/Indexing/Primary", fs{ 311 "Type": Map, 312 "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, 313 ast{"Compound/Indexing/Primary", fs{ 314 "Type": Map, 315 "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, 316 ast{"Compound/Indexing/Primary", fs{ 317 "Type": Map, 318 "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, 319 ast{"Compound/Indexing/Primary", fs{ 320 "Type": Map, 321 "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, 322 ast{"Compound/Indexing/Primary", fs{ 323 "Type": Map, 324 "MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}}, 325 ast{"Compound/Indexing/Primary", fs{ 326 "Type": Map, 327 "MapPairs": []ast{ 328 {"MapPair", fs{"Key": "a", "Value": "b"}}, 329 {"MapPair", fs{"Key": "c", "Value": "d"}}, 330 {"MapPair", fs{"Key": "e", "Value": "f"}}, 331 }}}, 332 ), 333 }, 334 { 335 name: "empty map", 336 code: "a [&] [ &] [& ] [ & ]", 337 node: &Chunk{}, 338 want: a( 339 ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, 340 ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, 341 ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, 342 ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}}, 343 ), 344 }, 345 { 346 name: "lambda without signature", 347 code: "{ echo}", 348 node: &Primary{}, 349 want: ast{"Primary", fs{ 350 "Type": Lambda, 351 "Chunk": "echo", 352 }}, 353 }, 354 { 355 name: "new-style lambda with arguments and options", 356 code: "{|a b &k=v| echo}", 357 node: &Primary{}, 358 want: ast{"Primary", fs{ 359 "Type": Lambda, 360 "Elements": []string{"a", "b"}, 361 "MapPairs": []string{"&k=v"}, 362 "Chunk": " echo", 363 }}, 364 }, 365 { 366 name: "output capture", 367 code: "a () (b;c) (c\nd)", 368 node: &Chunk{}, 369 want: a( 370 ast{"Compound/Indexing/Primary", fs{ 371 "Type": OutputCapture, "Chunk": ""}}, 372 ast{"Compound/Indexing/Primary", fs{ 373 "Type": OutputCapture, "Chunk": ast{ 374 "Chunk", fs{"Pipelines": []string{"b", "c"}}, 375 }}}, 376 ast{"Compound/Indexing/Primary", fs{ 377 "Type": OutputCapture, "Chunk": ast{ 378 "Chunk", fs{"Pipelines": []string{"c", "d"}}, 379 }}}, 380 ), 381 }, 382 { 383 name: "exception capture", 384 code: "a ?() ?(b;c)", 385 node: &Chunk{}, 386 want: a( 387 ast{"Compound/Indexing/Primary", fs{ 388 "Type": ExceptionCapture, "Chunk": ""}}, 389 ast{"Compound/Indexing/Primary", fs{ 390 "Type": ExceptionCapture, "Chunk": "b;c", 391 }}), 392 }, 393 { 394 name: "braced list", 395 code: "{,a,c\ng\n}", 396 node: &Primary{}, 397 want: ast{"Primary", fs{ 398 "Type": Braced, 399 "Braced": []string{"", "a", "c", "g", ""}}}, 400 }, 401 { 402 name: "tilde", 403 code: "~xiaq/go", 404 node: &Compound{}, 405 want: ast{"Compound", fs{ 406 "Indexings": []ast{ 407 {"Indexing/Primary", fs{"Type": Tilde, "Value": "~"}}, 408 {"Indexing/Primary", fs{"Type": Bareword, "Value": "xiaq/go"}}, 409 }, 410 }}, 411 }, 412 { 413 name: "tilde and wildcard", 414 code: "~xiaq/*.go", 415 node: &Compound{}, 416 want: ast{"Compound", fs{ 417 "Indexings": []ast{ 418 {"Indexing/Primary", fs{"Type": Tilde, "Value": "~"}}, 419 {"Indexing/Primary", fs{"Type": Bareword, "Value": "xiaq/"}}, 420 {"Indexing/Primary", fs{"Type": Wildcard, "Value": "*"}}, 421 {"Indexing/Primary", fs{"Type": Bareword, "Value": ".go"}}, 422 }, 423 }}, 424 }, 425 426 { 427 name: "unterminated single-quoted string", 428 code: "'a", 429 node: &Chunk{}, 430 wantErrAtEnd: true, 431 wantErrMsg: "string not terminated", 432 }, 433 { 434 name: "unterminated double-quoted string", 435 code: `"a`, 436 node: &Chunk{}, 437 wantErrAtEnd: true, 438 wantErrMsg: "string not terminated", 439 }, 440 { 441 name: "invalid control sequence", 442 code: `a "\^` + "\t", 443 node: &Chunk{}, 444 wantErrPart: "\t", 445 wantErrMsg: "invalid control sequence, should be a codepoint between 0x3F and 0x5F", 446 }, 447 { 448 name: "invalid hex escape sequence", 449 code: `a "\xQQ"`, 450 node: &Chunk{}, 451 wantErrPart: "Q", 452 wantErrMsg: "invalid escape sequence, should be hex digit", 453 }, 454 { 455 name: "invalid octal escape sequence", 456 code: `a "\1ab"`, 457 node: &Chunk{}, 458 wantErrPart: "a", 459 wantErrMsg: "invalid escape sequence, should be octal digit", 460 }, 461 { 462 name: "overflow in octal escape sequence", 463 code: `a "\400"`, 464 node: &Chunk{}, 465 wantErrPart: "\\400", 466 wantErrMsg: "invalid octal escape sequence, should be below 256", 467 }, 468 { 469 name: "invalid single-char escape sequence", 470 code: `a "\i"`, 471 node: &Chunk{}, 472 wantErrPart: "i", 473 wantErrMsg: "invalid escape sequence", 474 }, 475 { 476 name: "unterminated variable name", 477 code: "$", 478 node: &Chunk{}, 479 wantErrAtEnd: true, 480 wantErrMsg: "should be variable name", 481 }, 482 { 483 name: "list-map hybrid not supported", 484 code: "a [a &k=v]", 485 node: &Chunk{}, 486 // TODO(xiaq): Add correct position information. 487 wantErrAtEnd: true, 488 wantErrMsg: "cannot contain both list elements and map pairs", 489 }, 490 491 // Line continuation 492 { 493 name: "line continuation", 494 code: "a b^\nc", 495 node: &Chunk{}, 496 want: ast{ 497 "Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}, 498 }, 499 { 500 name: "unterminated line continuation", 501 code: `a ^`, 502 node: &Chunk{}, 503 wantErrAtEnd: true, 504 wantErrMsg: "should be newline", 505 }, 506 507 // Carriage return 508 { 509 name: "carriage return separating pipelines", 510 code: "a\rb", 511 node: &Chunk{}, 512 want: ast{"Chunk", fs{"Pipelines": []string{"a", "b"}}}, 513 }, 514 { 515 name: "carriage return + newline separating pipelines", 516 code: "a\r\nb", 517 node: &Chunk{}, 518 want: ast{"Chunk", fs{"Pipelines": []string{"a", "b"}}}, 519 }, 520 { 521 name: "carriage return as whitespace padding in lambdas", 522 code: "a { \rfoo\r\nbar }", 523 node: &Chunk{}, 524 want: a( 525 ast{"Compound/Indexing/Primary", 526 fs{"Type": Lambda, "Chunk": "foo\r\nbar "}}, 527 ), 528 }, 529 { 530 name: "carriage return separating elements in a lists", 531 code: "a [a\rb]", 532 node: &Chunk{}, 533 want: a( 534 ast{"Compound/Indexing/Primary", fs{ 535 "Type": List, 536 "Elements": []string{"a", "b"}}}), 537 }, 538 { 539 name: "carriage return in line continuation", 540 code: "a b^\rc", 541 node: &Chunk{}, 542 want: ast{ 543 "Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}, 544 }, 545 { 546 name: "carriage return + newline as a single newline in line continuation", 547 code: "a b^\r\nc", 548 node: &Chunk{}, 549 want: ast{ 550 "Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}, 551 }, 552 553 // Comment 554 { 555 name: "comments in chunks", 556 code: "a#haha\nb#lala", 557 node: &Chunk{}, 558 want: ast{ 559 "Chunk", fs{"Pipelines": []ast{ 560 {"Pipeline/Form", fs{"Head": "a"}}, 561 {"Pipeline/Form", fs{"Head": "b"}}, 562 }}}, 563 }, 564 { 565 name: "comments in lists", 566 code: "a [a#haha\nb]", 567 node: &Chunk{}, 568 want: a( 569 ast{"Compound/Indexing/Primary", fs{ 570 "Type": List, 571 "Elements": []string{"a", "b"}, 572 }}, 573 ), 574 }, 575 576 // Other errors 577 { 578 name: "unmatched )", 579 code: ")", 580 node: &Chunk{}, 581 wantErrPart: ")", 582 wantErrMsg: "unexpected rune ')'", 583 }, 584 { 585 name: "unmatched ]", 586 code: "]", 587 node: &Chunk{}, 588 wantErrPart: "]", 589 wantErrMsg: "unexpected rune ']'", 590 }, 591 { 592 name: "unmatched }", 593 code: "}", 594 node: &Chunk{}, 595 wantErrPart: "}", 596 wantErrMsg: "unexpected rune '}'", 597 }, 598 { 599 name: "unmatched (", 600 code: "a (", 601 node: &Chunk{}, 602 wantErrAtEnd: true, 603 wantErrMsg: "should be ')'", 604 }, 605 { 606 name: "unmatched [", 607 code: "a [", 608 node: &Chunk{}, 609 wantErrAtEnd: true, 610 wantErrMsg: "should be ']'", 611 }, 612 { 613 name: "unmatched {", 614 code: "a {", 615 node: &Chunk{}, 616 wantErrAtEnd: true, 617 wantErrMsg: "should be ',' or '}'", 618 }, 619 { 620 name: "unmatched { in lambda", 621 code: "a { ", 622 node: &Chunk{}, 623 wantErrAtEnd: true, 624 wantErrMsg: "should be '}'", 625 }, 626 { 627 name: "unmatched [ in indexing expression", 628 code: "a $a[0}", 629 node: &Chunk{}, 630 wantErrPart: "}", 631 wantErrMsg: "should be ']'", 632 }, 633 } 634 635 func TestParse(t *testing.T) { 636 for _, test := range testCases { 637 t.Run(test.name, func(t *testing.T) { 638 n := test.node 639 src := SourceForTest(test.code) 640 err := ParseAs(src, n, Config{}) 641 if test.wantErrMsg == "" { 642 if err != nil { 643 t.Errorf("Parse(%q) returns error: %v", test.code, err) 644 } 645 err = checkParseTree(n) 646 if err != nil { 647 t.Errorf("Parse(%q) returns bad parse tree: %v", test.code, err) 648 fmt.Fprintf(os.Stderr, "Parse tree of %q:\n", test.code) 649 pprintParseTree(n, os.Stderr) 650 } 651 err = checkAST(n, test.want) 652 if err != nil { 653 t.Errorf("Parse(%q) returns bad AST: %v", test.code, err) 654 fmt.Fprintf(os.Stderr, "AST of %q:\n", test.code) 655 pprintAST(n, os.Stderr) 656 } 657 } else { 658 if err == nil { 659 t.Errorf("Parse(%q) returns no error, want error with %q", 660 test.code, test.wantErrMsg) 661 } 662 parseError := err.(*Error).Entries[0] 663 r := parseError.Context 664 665 if errPart := test.code[r.From:r.To]; errPart != test.wantErrPart { 666 t.Errorf("Parse(%q) returns error with part %q, want %q", 667 test.code, errPart, test.wantErrPart) 668 } 669 if atEnd := r.From == len(test.code); atEnd != test.wantErrAtEnd { 670 t.Errorf("Parse(%q) returns error at end = %v, want %v", 671 test.code, atEnd, test.wantErrAtEnd) 672 } 673 if errMsg := parseError.Message; errMsg != test.wantErrMsg { 674 t.Errorf("Parse(%q) returns error with message %q, want %q", 675 test.code, errMsg, test.wantErrMsg) 676 } 677 } 678 }) 679 } 680 } 681 682 func TestParse_ReturnsTreeContainingSourceFromArgument(t *testing.T) { 683 src := SourceForTest("a") 684 tree, _ := Parse(src, Config{}) 685 if tree.Source != src { 686 t.Errorf("tree.Source = %v, want %v", tree.Source, src) 687 } 688 }