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  }