github.com/aretext/aretext@v1.3.0/syntax/languages/bash_test.go (about) 1 package languages 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/assert" 7 8 "github.com/aretext/aretext/syntax/parser" 9 ) 10 11 func TestBashParseFunc(t *testing.T) { 12 testCases := []struct { 13 name string 14 text string 15 expected []TokenWithText 16 }{ 17 { 18 name: "comment", 19 text: "# this is a comment", 20 expected: []TokenWithText{ 21 { 22 Role: parser.TokenRoleComment, 23 Text: "# this is a comment", 24 }, 25 }, 26 }, 27 { 28 name: "if condition", 29 text: ` 30 if $var; then 31 echo "hello"; 32 fi`, 33 expected: []TokenWithText{ 34 {Role: parser.TokenRoleKeyword, Text: `if`}, 35 {Role: bashTokenRoleVariable, Text: `$var`}, 36 {Role: parser.TokenRoleKeyword, Text: `then`}, 37 {Role: parser.TokenRoleString, Text: `"hello"`}, 38 {Role: parser.TokenRoleKeyword, Text: `fi`}, 39 }, 40 }, 41 { 42 name: "while loop", 43 text: ` 44 while $var; do 45 echo "hello"; 46 done`, 47 expected: []TokenWithText{ 48 {Role: parser.TokenRoleKeyword, Text: `while`}, 49 {Role: bashTokenRoleVariable, Text: `$var`}, 50 {Role: parser.TokenRoleKeyword, Text: `do`}, 51 {Role: parser.TokenRoleString, Text: `"hello"`}, 52 {Role: parser.TokenRoleKeyword, Text: `done`}, 53 }, 54 }, 55 { 56 name: "case statement", 57 text: ` 58 case $var in 59 foo) echo "hello" 60 bar) echo "goodbye" 61 *) echo "ok" 62 esac`, 63 expected: []TokenWithText{ 64 {Role: parser.TokenRoleKeyword, Text: `case`}, 65 {Role: bashTokenRoleVariable, Text: `$var`}, 66 {Role: parser.TokenRoleKeyword, Text: `in`}, 67 {Role: parser.TokenRoleString, Text: `"hello"`}, 68 {Role: parser.TokenRoleString, Text: `"goodbye"`}, 69 {Role: parser.TokenRoleString, Text: `"ok"`}, 70 {Role: parser.TokenRoleKeyword, Text: `esac`}, 71 }, 72 }, 73 { 74 name: "variable followed by hyphen", 75 text: "$FOO-bar", 76 expected: []TokenWithText{ 77 {Role: bashTokenRoleVariable, Text: `$FOO`}, 78 }, 79 }, 80 { 81 name: "variable brace expansion", 82 text: `${PATH:-}`, 83 expected: []TokenWithText{ 84 {Role: bashTokenRoleVariable, Text: `${PATH:-}`}, 85 }, 86 }, 87 { 88 name: "variable brace expansion with quoted close brace", 89 text: `${FOO:-"close with }"}`, 90 expected: []TokenWithText{ 91 {Role: bashTokenRoleVariable, Text: `${FOO:-"close with }"}`}, 92 }, 93 }, 94 { 95 name: "variable positional argument", 96 text: `[ $# -ne 2 ] && { echo "Usage: $0 VERSION NAME"; exit 1; }`, 97 expected: []TokenWithText{ 98 {Role: bashTokenRoleVariable, Text: `$#`}, 99 {Role: parser.TokenRoleOperator, Text: `&&`}, 100 {Role: parser.TokenRoleString, Text: `"Usage: $0 VERSION NAME"`}, 101 }, 102 }, 103 { 104 name: "subshell", 105 text: `echo $(pwd)`, 106 expected: []TokenWithText{ 107 {Role: parser.TokenRoleOperator, Text: `$`}, 108 }, 109 }, 110 { 111 name: "backquote expansion", 112 text: "rm `find . -name '*.go'`", 113 expected: []TokenWithText{ 114 {Role: bashTokenRoleBackquoteExpansion, Text: "`find . -name '*.go'`"}, 115 }, 116 }, 117 { 118 name: "file redirect", 119 text: "go test > out.txt", 120 expected: []TokenWithText{ 121 {Role: parser.TokenRoleOperator, Text: ">"}, 122 }, 123 }, 124 { 125 name: "file redirect with ampersand", 126 text: "go test &> out.txt", 127 expected: []TokenWithText{ 128 {Role: parser.TokenRoleOperator, Text: "&>"}, 129 }, 130 }, 131 { 132 name: "pipe", 133 text: `echo "foo" | wl-copy`, 134 expected: []TokenWithText{ 135 {Role: parser.TokenRoleString, Text: `"foo"`}, 136 {Role: parser.TokenRoleOperator, Text: `|`}, 137 }, 138 }, 139 { 140 name: "regex match", 141 text: `[[ $line =~ [[:space:]]*(a)?b ]]`, 142 expected: []TokenWithText{ 143 {Role: bashTokenRoleVariable, Text: `$line`}, 144 {Role: parser.TokenRoleOperator, Text: `=~`}, 145 }, 146 }, 147 { 148 name: "not condition", 149 text: `if ! grep $foo; then echo "not found"; fi`, 150 expected: []TokenWithText{ 151 {Role: parser.TokenRoleKeyword, Text: `if`}, 152 {Role: parser.TokenRoleOperator, Text: `!`}, 153 {Role: bashTokenRoleVariable, Text: `$foo`}, 154 {Role: parser.TokenRoleKeyword, Text: `then`}, 155 {Role: parser.TokenRoleString, Text: `"not found"`}, 156 {Role: parser.TokenRoleKeyword, Text: `fi`}, 157 }, 158 }, 159 { 160 name: "double quote escaped quote", 161 text: `"abcd \" xyz"`, 162 expected: []TokenWithText{ 163 {Role: parser.TokenRoleString, Text: `"abcd \" xyz"`}, 164 }, 165 }, 166 { 167 name: "double quote string multi-line", 168 text: `FOO=" 169 a 170 b 171 c"`, 172 expected: []TokenWithText{ 173 {Role: parser.TokenRoleOperator, Text: `=`}, 174 {Role: parser.TokenRoleString, Text: "\"\na\nb\nc\""}, 175 }, 176 }, 177 { 178 name: "double quote variable expansion", 179 text: `"var=$VAR"`, 180 expected: []TokenWithText{ 181 {Role: parser.TokenRoleString, Text: `"var=$VAR"`}, 182 }, 183 }, 184 { 185 name: "escaped dollar sign before variable expansion", 186 text: `\$${PATH}`, 187 expected: []TokenWithText{ 188 {Role: bashTokenRoleVariable, Text: "${PATH}"}, 189 }, 190 }, 191 { 192 name: "single quote string", 193 text: `'abc defgh'`, 194 expected: []TokenWithText{ 195 {Role: parser.TokenRoleString, Text: `'abc defgh'`}, 196 }, 197 }, 198 { 199 name: "double quote string with subshell expansion", 200 text: `echo "echo $(echo "\"foo\"")"`, 201 expected: []TokenWithText{ 202 {Role: parser.TokenRoleString, Text: `"echo $(echo "\"foo\"")"`}, 203 }, 204 }, 205 { 206 name: "double quote string with variable expansion", 207 text: `echo "${FOO:-"foo"}"`, 208 expected: []TokenWithText{ 209 {Role: parser.TokenRoleString, Text: `"${FOO:-"foo"}"`}, 210 }, 211 }, 212 { 213 name: "double quote string with backquote expansion", 214 text: "echo \"`echo \"hello\"`\"", 215 expected: []TokenWithText{ 216 {Role: parser.TokenRoleString, Text: "\"`echo \"hello\"`\""}, 217 }, 218 }, 219 { 220 name: "double quote string with escaped $ then variable expansion", 221 text: `echo "\$${PATH}"`, 222 expected: []TokenWithText{ 223 {Role: parser.TokenRoleString, Text: `"\$${PATH}"`}, 224 }, 225 }, 226 { 227 name: "unterminated double quote", 228 text: `echo "`, 229 expected: []TokenWithText{}, 230 }, 231 { 232 name: "unterminated single quote", 233 text: `echo '`, 234 expected: []TokenWithText{}, 235 }, 236 { 237 name: "unterminated backquote", 238 text: "echo `", 239 expected: []TokenWithText{}, 240 }, 241 { 242 name: "heredoc", 243 text: ` 244 cat << EOF 245 this is 246 some heredoc 247 text 248 EOF 249 `, 250 expected: []TokenWithText{ 251 {Role: parser.TokenRoleOperator, Text: `<<`}, 252 {Role: parser.TokenRoleString, Text: `EOF 253 this is 254 some heredoc 255 text 256 EOF`}, 257 }, 258 }, 259 { 260 name: "heredoc indented end word", 261 text: ` 262 cat << EOF 263 this is 264 some heredoc 265 text 266 EOF 267 EOF 268 `, 269 expected: []TokenWithText{ 270 {Role: parser.TokenRoleOperator, Text: `<<`}, 271 {Role: parser.TokenRoleString, Text: `EOF 272 this is 273 some heredoc 274 text 275 EOF 276 EOF`}, 277 }, 278 }, 279 { 280 name: "heredoc dash then word without whitespace", 281 text: ` 282 cat <<<-FOO 283 heredoc text 284 FOO 285 `, 286 expected: []TokenWithText{ 287 {Role: parser.TokenRoleOperator, Text: `<<<-`}, 288 {Role: parser.TokenRoleString, Text: `FOO 289 heredoc text 290 FOO`}, 291 }, 292 }, 293 { 294 name: "heredoc contains end word prefix", 295 text: ` 296 cat << EOF 297 EOFANDTHENSOME 298 EOF AND THEN SOME 299 EOF 300 `, 301 expected: []TokenWithText{ 302 {Role: parser.TokenRoleOperator, Text: `<<`}, 303 {Role: parser.TokenRoleString, Text: `EOF 304 EOFANDTHENSOME 305 EOF AND THEN SOME 306 EOF`}, 307 }, 308 }, 309 { 310 name: "heredoc contains partial end word", 311 text: ` 312 cat << ENDWORD 313 END 314 ENDWORD 315 `, 316 expected: []TokenWithText{ 317 {Role: parser.TokenRoleOperator, Text: `<<`}, 318 {Role: parser.TokenRoleString, Text: `ENDWORD 319 END 320 ENDWORD`}, 321 }, 322 }, 323 { 324 name: "heredoc no word", 325 text: ` 326 cat << 327 echo "hello" 328 `, 329 expected: []TokenWithText{ 330 {Role: parser.TokenRoleOperator, Text: `<<`}, 331 {Role: parser.TokenRoleString, Text: `"hello"`}, 332 }, 333 }, 334 { 335 name: "heredoc EOF before word", 336 text: `cat <<`, 337 expected: []TokenWithText{ 338 {Role: parser.TokenRoleOperator, Text: `<<`}, 339 }, 340 }, 341 { 342 name: "heredoc single-quoted word", 343 text: ` 344 cat << 'EOF' 345 heredoc text 346 EOF`, 347 expected: []TokenWithText{ 348 {Role: parser.TokenRoleOperator, Text: `<<`}, 349 {Role: parser.TokenRoleString, Text: `'EOF' 350 heredoc text 351 EOF`}, 352 }, 353 }, 354 { 355 name: "heredoc double-quoted word", 356 text: ` 357 cat << "EOF" 358 heredoc text 359 EOF`, 360 expected: []TokenWithText{ 361 {Role: parser.TokenRoleOperator, Text: `<<`}, 362 {Role: parser.TokenRoleString, Text: `"EOF" 363 heredoc text 364 EOF`}, 365 }, 366 }, 367 { 368 name: "heredoc empty quoted word", 369 text: ` 370 cat << "" 371 heredoc text 372 373 echo 'hello'`, 374 expected: []TokenWithText{ 375 {Role: parser.TokenRoleOperator, Text: `<<`}, 376 {Role: parser.TokenRoleString, Text: `"" 377 heredoc text 378 `}, 379 {Role: parser.TokenRoleString, Text: `'hello'`}, 380 }, 381 }, 382 { 383 name: "heredoc one single quote", 384 text: ` 385 cat << ' 386 foo`, 387 expected: []TokenWithText{ 388 {Role: parser.TokenRoleOperator, Text: `<<`}, 389 }, 390 }, 391 { 392 name: "heredoc one double quote", 393 text: ` 394 cat << " 395 foo`, 396 expected: []TokenWithText{ 397 {Role: parser.TokenRoleOperator, Text: `<<`}, 398 }, 399 }, 400 { 401 name: "heredoc backslash quoted word", 402 text: ` 403 cat << \EOF 404 heredoc text 405 EOF`, 406 expected: []TokenWithText{ 407 {Role: parser.TokenRoleOperator, Text: `<<`}, 408 {Role: parser.TokenRoleString, Text: `\EOF 409 heredoc text 410 EOF`}, 411 }, 412 }, 413 { 414 name: "heredoc backslash empty word", 415 text: ` 416 cat << \ 417 heredoc text 418 EOF`, 419 expected: []TokenWithText{ 420 {Role: parser.TokenRoleOperator, Text: `<<`}, 421 {Role: parser.TokenRoleString, Text: `\ 422 heredoc text 423 `}, 424 }, 425 }, 426 { 427 name: "function name with dash", 428 text: `foo-for-bar() { echo "foo bar" }`, 429 expected: []TokenWithText{ 430 {Role: parser.TokenRoleString, Text: `"foo bar"`}, 431 }, 432 }, 433 { 434 name: "variable assignment with home expansion", 435 text: `path=~/foo/bar`, 436 expected: []TokenWithText{ 437 {Role: parser.TokenRoleOperator, Text: `=`}, 438 }, 439 }, 440 { 441 name: "append operator", 442 text: `GLOB+="foo"`, 443 expected: []TokenWithText{ 444 {Role: parser.TokenRoleOperator, Text: `+=`}, 445 {Role: parser.TokenRoleString, Text: `"foo"`}, 446 }, 447 }, 448 { 449 name: "conditional with regex start of line", 450 text: `[[ $line =~ ^"initial string" ]]`, 451 expected: []TokenWithText{ 452 {Role: bashTokenRoleVariable, Text: `$line`}, 453 {Role: parser.TokenRoleOperator, Text: `=~`}, 454 {Role: parser.TokenRoleOperator, Text: `^`}, 455 {Role: parser.TokenRoleString, Text: `"initial string"`}, 456 }, 457 }, 458 { 459 name: "conditional with exact string match", 460 text: `[[ $line == "test" ]]`, 461 expected: []TokenWithText{ 462 {Role: bashTokenRoleVariable, Text: `$line`}, 463 {Role: parser.TokenRoleOperator, Text: `==`}, 464 {Role: parser.TokenRoleString, Text: `"test"`}, 465 }, 466 }, 467 { 468 name: "conditional with lexicographic order comparison", 469 text: `[[ $line > "test" ]]`, 470 expected: []TokenWithText{ 471 {Role: bashTokenRoleVariable, Text: `$line`}, 472 {Role: parser.TokenRoleOperator, Text: `>`}, 473 {Role: parser.TokenRoleString, Text: `"test"`}, 474 }, 475 }, 476 { 477 name: "if statement with conditional", 478 text: `if [[ $line == "test"]]; then x=~/foo/bar; fi`, 479 expected: []TokenWithText{ 480 {Role: parser.TokenRoleKeyword, Text: `if`}, 481 {Role: bashTokenRoleVariable, Text: `$line`}, 482 {Role: parser.TokenRoleOperator, Text: `==`}, 483 {Role: parser.TokenRoleString, Text: `"test"`}, 484 {Role: parser.TokenRoleKeyword, Text: `then`}, 485 {Role: parser.TokenRoleOperator, Text: `=`}, 486 {Role: parser.TokenRoleKeyword, Text: `fi`}, 487 }, 488 }, 489 } 490 491 for _, tc := range testCases { 492 t.Run(tc.name, func(t *testing.T) { 493 tokens := ParseTokensWithText(BashParseFunc(), tc.text) 494 assert.Equal(t, tc.expected, tokens) 495 }) 496 } 497 } 498 499 func FuzzBashParseFunc(f *testing.F) { 500 seeds := LoadFuzzTestSeeds(f, "./testdata/bash/*") 501 FuzzParser(f, BashParseFunc(), seeds) 502 }