OILS / test / parse-errors.sh View on Github | oils.pub

854 lines, 418 significant
1#!/usr/bin/env bash
2#
3# Usage:
4# test/parse-errors.sh <function name>
5
6set -o nounset
7set -o pipefail
8set -o errexit
9
10source test/common.sh
11source test/sh-assert.sh # _assert-sh-status
12
13# We can't really run with OSH=bash, because the exit status is often different
14
15# Although it would be nice to IGNORE errors and them some how preview errors.
16
17OSH=${OSH:-bin/osh}
18YSH=${YSH:-bin/ysh}
19
20# More detailed assertions - TODO: remove these?
21
22_assert-status-2() {
23 ### An interface where you can pass flags like -O test-no_parse_backslash
24
25 local message=$0
26 _assert-sh-status 2 $OSH $message "$@"
27}
28
29_assert-status-2-here() {
30 _assert-status-2 "$@" -c "$(cat)"
31}
32
33_runtime-parse-error() {
34 ### Assert that a parse error happens at runtime, e.g. for [ z z ]
35
36 _osh-error-X 2 "$@"
37}
38
39#
40# Cases
41#
42
43test-syntax-abbrev() {
44 # test frontend/syntax_abbrev.py
45
46 local o1='echo "double $x ${braced}" $(date)'
47 _osh-should-parse "$o1"
48 $OSH --ast-format text -n -c "$o1"
49
50 local o2="echo 'single'"
51 _osh-should-parse "$o2"
52 $OSH --ast-format text -n -c "$o2"
53
54 local y1='var x = i + 42;'
55 _ysh-should-parse "$y1"
56 $YSH --ast-format text -n -c "$y1"
57}
58
59# All in osh/word_parse.py
60test-patsub() {
61 _osh-should-parse 'echo ${x/}'
62 _osh-should-parse 'echo ${x//}'
63
64 _osh-should-parse 'echo ${x/foo}' # pat 'foo', no mode, replace empty
65
66 _osh-should-parse 'echo ${x//foo}' # pat 'foo', replace mode '/', replace empty
67 _osh-should-parse 'echo ${x/%foo}' # same as above
68
69 _osh-should-parse 'echo ${x///foo}'
70
71 _osh-should-parse 'echo ${x///}' # found and fixed bug
72 _osh-should-parse 'echo ${x/%/}' # pat '', replace mode '%', replace ''
73
74 _osh-should-parse 'echo ${x////}' # pat '/', replace mode '/', replace empty
75 _osh-should-parse 'echo ${x/%//}' # pat '', replace mode '%', replace '/'
76
77 # Newline in replacement pattern
78 _osh-should-parse 'echo ${x//foo/replace
79}'
80 _osh-should-parse 'echo ${x//foo/replace$foo}'
81}
82
83test-slice() {
84 _osh-should-parse '${foo:42}'
85 _osh-should-parse '${foo:42+1}'
86
87 # Slicing
88 _osh-parse-error 'echo ${a:1;}'
89 _osh-parse-error 'echo ${a:1:2;}'
90}
91
92# osh/word_parse.py
93test-word-parse() {
94 _osh-parse-error 'echo ${'
95
96 _osh-parse-error 'echo ${a[@Z'
97
98 _osh-parse-error 'echo ${x.}'
99 _osh-parse-error 'echo ${!x.}'
100
101 # I don't seem to be able to tickle errors here
102 #_osh-parse-error 'echo ${a:-}'
103 #_osh-parse-error 'echo ${a#}'
104
105 _osh-parse-error 'echo ${#a.'
106
107 # for (( ))
108 _osh-parse-error 'for (( i = 0; i < 10; i++ ;'
109 # Hm not sure about this
110 _osh-parse-error 'for (( i = 0; i < 10; i++ /'
111
112 _osh-parse-error 'echo @(extglob|foo'
113
114 # Copied from osh/word_parse_test.py. Bugs were found while writing
115 # core/completion_test.py.
116
117 _osh-parse-error '${undef:-'
118 _osh-parse-error '${undef:-$'
119 _osh-parse-error '${undef:-$F'
120
121 _osh-parse-error '${x@'
122 _osh-parse-error '${x@Q'
123
124 _osh-parse-error '${x%'
125
126 _osh-parse-error '${x/'
127 _osh-parse-error '${x/a/'
128 _osh-parse-error '${x/a/b'
129 _osh-parse-error '${x:'
130}
131
132test-dparen() {
133 # (( ))
134
135 _osh-should-parse '(())'
136 _osh-should-parse '(( ))'
137 _osh-parse-error '(( )'
138 _osh-parse-error '(( )x'
139 #_osh-should-parse '$(echo $(( 1 + 2 )) )'
140
141 # Hard case
142 _osh-should-parse '$(echo $(( 1 + 2 )))'
143 _osh-should-parse '$( (()))'
144
145 # More
146 _osh-parse-error '(( 1 + 2 /'
147 _osh-parse-error '(( 1 + 2 )/'
148 _osh-parse-error '(( 1'
149 _osh-parse-error '(('
150}
151
152test-arith-sub() {
153 # $(( ))
154
155 _osh-should-parse 'echo $(( ))'
156 _osh-should-parse 'echo $(())'
157 _osh-parse-error 'echo $(()x'
158
159 _osh-parse-error 'echo $(()'
160
161 _osh-parse-error 'echo $(( 1 + 2 ;'
162 _osh-parse-error 'echo $(( 1 + 2 );'
163 _osh-parse-error 'echo $(( '
164 _osh-parse-error 'echo $(( 1'
165}
166
167
168test-array-literal() {
169 # Array literal with invalid TokenWord.
170 _osh-parse-error 'a=(1 & 2)'
171 _osh-parse-error 'a= (1 2)'
172 _osh-parse-error 'a=(1 2'
173 _osh-parse-error 'a=(1 ${2@} )' # error in word inside array literal
174}
175
176test-arith-context() {
177 # Non-standard arith sub $[1 + 2]
178 _osh-parse-error 'echo $[ 1 + 2 ;'
179
180 # What's going on here? No location info?
181 _osh-parse-error 'echo $[ 1 + 2 /'
182
183 _osh-parse-error 'echo $[ 1 + 2 / 3'
184 _osh-parse-error 'echo $['
185
186 # this is currently dynamic arithmetic
187 _osh-parse-error 'a[x+]=1'
188
189 # Wrap it in eval too
190 _osh-error-2 'eval a[x+]=1'
191
192 _osh-parse-error 'a[]=1'
193
194 _osh-parse-error 'a[*]=1'
195
196 # These errors are different because the arithmetic lexer mode has } but not
197 # {. May be changed later.
198 _osh-parse-error '(( a + { ))'
199 _osh-parse-error '(( a + } ))'
200
201}
202
203test-arith-integration() {
204 # Regression: these were not parse errors, but should be!
205 _osh-parse-error 'echo $((a b))'
206 _osh-parse-error '((a b))'
207
208 # Empty arithmetic expressions
209 _osh-should-parse 'for ((x=0; x<5; x++)); do echo $x; done'
210 _osh-should-parse 'for ((; x<5; x++)); do echo $x; done'
211 _osh-should-parse 'for ((; ; x++)); do echo $x; done'
212 _osh-should-parse 'for ((; ;)); do echo $x; done'
213
214 # Extra tokens on the end of each expression
215 _osh-parse-error 'for ((x=0; x<5; x++ b)); do echo $x; done'
216
217 _osh-parse-error 'for ((x=0 b; x<5; x++)); do echo $x; done'
218 _osh-parse-error 'for ((x=0; x<5 b; x++)); do echo $x; done'
219
220 _osh-parse-error '${a:1+2 b}'
221 _osh-parse-error '${a:1+2:3+4 b}'
222
223 _osh-parse-error '${a[1+2 b]}'
224}
225
226test-arith-expr() {
227 # BUG: the token is off here
228 _osh-parse-error '$(( 1 + + ))'
229
230 # BUG: not a great error either
231 _osh-parse-error '$(( 1 2 ))'
232
233 # Triggered a crash!
234 _osh-parse-error '$(( - ; ))'
235
236 # NOTE: This is confusing, should point to ` for command context?
237 _osh-parse-error '$(( ` ))'
238
239 _osh-parse-error '$(( $ ))'
240
241 # Invalid assignments
242 _osh-parse-error '$(( x+1 = 42 ))'
243 _osh-parse-error '$(( (x+42)++ ))'
244 _osh-parse-error '$(( ++(x+42) ))'
245
246 # Note these aren't caught because '1' is an ArithWord like 0x$x
247 #_osh-parse-error '$(( 1 = foo ))'
248 #_osh-parse-error '$(( 1++ ))'
249 #_osh-parse-error '$(( ++1 ))'
250}
251
252test-command-sub() {
253 _osh-parse-error '
254 echo line 2
255 echo $( echo '
256 _osh-parse-error '
257 echo line 2
258 echo ` echo '
259
260 # This is source.Reparsed('backticks', ...)
261
262 # Both unclosed
263 _osh-parse-error '
264 echo line 2
265 echo ` echo \` '
266
267 # Only the inner one is unclosed
268 _osh-parse-error '
269 echo line 2
270 echo ` echo \`unclosed ` '
271
272 _osh-parse-error 'echo `for x in`'
273}
274
275test-bool-expr() {
276 # Extra word
277 _osh-parse-error '[[ a b ]]'
278 _osh-parse-error '[[ a "a"$(echo hi)"b" ]]'
279
280 # Wrong error message
281 _osh-parse-error '[[ a == ]]'
282
283 if false; then
284 # Invalid regex
285 # These are currently only detected at runtime.
286 _osh-parse-error '[[ $var =~ * ]]'
287 _osh-parse-error '[[ $var =~ + ]]'
288 fi
289
290 # Unbalanced parens
291 _osh-parse-error '[[ ( 1 == 2 - ]]'
292
293 _osh-parse-error '[[ == ]]'
294 _osh-parse-error '[[ ) ]]'
295 _osh-parse-error '[[ ( ]]'
296
297 _osh-parse-error '[[ ;;; ]]'
298 _osh-parse-error '[['
299
300 # Expected right )
301 _osh-parse-error '[[ ( a == b foo${var} ]]'
302}
303
304test-regex-nix() {
305 ### Based on Nix bug
306
307 # Nix idiom - added space
308 _osh-should-parse '
309if [[ ! (" ${params[*]} " =~ " -shared " || " ${params[*]} " =~ " -static " ) ]]; then
310 echo hi
311fi
312'
313
314 # (x) is part of the regex
315 _osh-should-parse '
316if [[ (foo =~ (x) ) ]]; then
317 echo hi
318fi
319'
320 # Nix idiom - reduced
321 _osh-should-parse '
322if [[ (foo =~ x) ]]; then
323 echo hi
324fi
325'
326
327 # Nix idiom - original
328 _osh-should-parse '
329if [[ ! (" ${params[*]} " =~ " -shared " || " ${params[*]} " =~ " -static ") ]]; then
330 echo hi
331fi
332'
333}
334
335test-regex-pipe() {
336 # Pipe in outer expression - it becomes Lit_Other, which is fine
337
338 # Well we need a special rule for this probably
339 local s='[[ a =~ b|c ]]'
340 bash -n -c "$s"
341 _osh-should-parse "$s"
342}
343
344test-regex-space() {
345 # initial space
346 _osh-should-parse '[[ a =~ ( ) ]]'
347 _osh-should-parse '[[ a =~ (b c) ]]'
348 _osh-should-parse '[[ a =~ (a b)(c d) ]]'
349
350 # Hm bash allows newline inside (), but not outside
351 # I feel like we don't need to duplicate this
352
353 local s='[[ a =~ (b
354c) ]]'
355 bash -n -c "$s"
356 echo bash=$?
357
358 _osh-should-parse "$s"
359}
360
361test-regex-right-paren() {
362 # BashRegex lexer mode
363 _osh-should-parse '[[ a =~ b ]]'
364 _osh-should-parse '[[ a =~ (b) ]]' # this is a regex
365 _osh-should-parse '[[ (a =~ b) ]]' # this is grouping
366 _osh-should-parse '[[ (a =~ (b)) ]]' # regex and grouping!
367
368 _osh-parse-error '[[ (a =~ (' # EOF
369 _osh-parse-error '[[ (a =~ (b' # EOF
370
371 return
372 # Similar thing for extglob
373 _osh-should-parse '[[ a == b ]]'
374 _osh-should-parse '[[ a == @(b) ]]' # this is a regex
375 _osh-should-parse '[[ (a == b) ]]' # this is grouping
376 _osh-should-parse '[[ (a == @(b)) ]]' # regex and grouping!
377}
378
379
380# These don't have any location information.
381test-test-builtin() {
382 # Some of these come from osh/bool_parse.py, and some from
383 # osh/builtin_bracket.py.
384
385 # Extra token
386 _runtime-parse-error '[ x -a y f ]'
387 _runtime-parse-error 'test x -a y f'
388
389 # Missing closing ]
390 _runtime-parse-error '[ x '
391
392 # Hm some of these errors are wonky. Need positions.
393 _runtime-parse-error '[ x x ]'
394
395 _runtime-parse-error '[ x x "a b" ]'
396
397 # This is a runtime error but is handled similarly
398 _runtime-parse-error '[ -t xxx ]'
399
400 _runtime-parse-error '[ \( x -a -y -a z ]'
401
402 # -o tests if an option is enabled.
403 #_osh-parse-error '[ -o x ]'
404}
405
406test-printf-builtin() {
407 _runtime-parse-error 'printf %'
408 _runtime-parse-error 'printf [%Z]'
409
410 _runtime-parse-error 'printf -v "-invalid-" %s foo'
411}
412
413test-other-builtins() {
414 _runtime-parse-error 'shift 1 2'
415 _runtime-parse-error 'shift zzz'
416
417 _runtime-parse-error 'pushd x y'
418 _runtime-parse-error 'pwd -x'
419
420 _runtime-parse-error 'pp x foo a-x'
421
422 _runtime-parse-error 'wait zzz'
423 _runtime-parse-error 'wait %jobspec-not-supported'
424
425 _runtime-parse-error 'unset invalid-var-name'
426 _runtime-parse-error 'getopts 'hc:' invalid-var-name'
427}
428
429test-quoted-strings() {
430 _osh-parse-error '"unterminated double'
431
432 _osh-parse-error "'unterminated single"
433
434 _osh-parse-error '
435 "unterminated double multiline
436 line 1
437 line 2'
438
439 _osh-parse-error "
440 'unterminated single multiline
441 line 1
442 line 2"
443}
444
445test-braced-var-sub() {
446 # These should have ! for a prefix query
447 _osh-parse-error 'echo ${x*}'
448 _osh-parse-error 'echo ${x@}'
449
450 _osh-parse-error 'echo ${x.}'
451}
452
453test-cmd-parse() {
454 _osh-parse-error 'FOO=1 break'
455 _osh-parse-error 'break 1 2'
456
457 _osh-parse-error 'x"y"() { echo hi; }'
458
459 _osh-parse-error 'function x"y" { echo hi; }'
460
461 _osh-parse-error '}'
462
463 _osh-parse-error 'case foo in *) echo '
464 _osh-parse-error 'case foo in x|) echo '
465
466 _osh-parse-error 'ls foo|'
467 _osh-parse-error 'ls foo&&'
468
469 _osh-parse-error 'foo()'
470
471 # no_parse_ignored
472 _osh-should-parse 'break >out'
473 _ysh-parse-error 'break >out'
474
475 # Unquoted (
476 _osh-parse-error '[ ( x ]'
477}
478
479test-append() {
480 # from spec/test-append.test.sh. bash treats this as a runtime error, but it's a
481 # parse error in OSH.
482 _osh-parse-error 'a[-1]+=(4 5)'
483}
484
485test-redirect() {
486 _osh-parse-error 'echo < <<'
487 _osh-parse-error 'echo $( echo > >> )'
488}
489
490test-simple-command() {
491 _osh-parse-error 'PYTHONPATH=. FOO=(1 2) python'
492 # not statically detected after dynamic assignment
493 #_osh-parse-error 'echo foo FOO=(1 2)'
494
495 _osh-parse-error 'PYTHONPATH+=1 python'
496}
497
498test-leading-equals() {
499 # allowed in OSH for compatibility
500 _osh-should-parse '=var'
501 _osh-should-parse '=a[i]'
502
503 # In YSH, avoid confusion with = var and = f(x)
504 _ysh-parse-error '=var'
505 _ysh-parse-error '=a[i]'
506}
507
508# Old code? All these pass
509DISABLED-assign() {
510 _osh-parse-error 'local name$x'
511 _osh-parse-error 'local "ab"'
512 _osh-parse-error 'local a.b'
513
514 _osh-parse-error 'FOO=1 local foo=1'
515}
516
517# I can't think of any other here doc error conditions except arith/var/command
518# substitution, and unterminated.
519test-here-doc() {
520 # Arith in here doc
521 _osh-parse-error 'cat <<EOF
522$(( 1 * ))
523EOF
524'
525
526 # Varsub in here doc
527 _osh-parse-error 'cat <<EOF
528invalid: ${a!}
529EOF
530'
531
532 _osh-parse-error 'cat <<EOF
533$(for x in )
534EOF
535'
536}
537
538test-here-doc-delimiter() {
539 # NOTE: This is more like the case where.
540 _osh-parse-error 'cat << $(invalid here end)'
541
542 # TODO: Arith parser doesn't have location information
543 _osh-parse-error 'cat << $((1+2))'
544 _osh-parse-error 'cat << a=(1 2 3)'
545 _osh-parse-error 'cat << \a$(invalid)'
546
547 # Actually the $invalid part should be highlighted... yeah an individual
548 # part is the problem.
549 #"cat << 'single'$(invalid)"
550 _osh-parse-error 'cat << "double"$(invalid)'
551 _osh-parse-error 'cat << ~foo/$(invalid)'
552 _osh-parse-error 'cat << $var/$(invalid)'
553}
554
555test-args-parse-builtin() {
556 _runtime-parse-error 'read -x' # invalid
557 _runtime-parse-error 'builtin read -x' # ditto
558
559 _runtime-parse-error 'read -n' # expected argument for -n
560 _runtime-parse-error 'read -n x' # expected integer
561
562 _runtime-parse-error 'set -o errexit +o oops'
563
564 # not implemented yet
565 #_osh-parse-error 'read -t x' # expected floating point number
566}
567
568test-args-parse-more() {
569 _runtime-parse-error 'set -z'
570 _runtime-parse-error 'shopt -s foo'
571 _runtime-parse-error 'shopt -z'
572}
573
574DISABLED-args-parse-main() {
575 $OSH --ast-format x
576
577 $OSH -o errexit +o oops
578}
579
580test-invalid-brace-ranges() {
581 _osh-parse-error 'echo {1..3..-1}'
582 _osh-parse-error 'echo {1..3..0}'
583 _osh-parse-error 'echo {3..1..1}'
584 _osh-parse-error 'echo {3..1..0}'
585 _osh-parse-error 'echo {a..Z}'
586 _osh-parse-error 'echo {a..z..0}'
587 _osh-parse-error 'echo {a..z..-1}'
588 _osh-parse-error 'echo {z..a..1}'
589}
590
591test-extra-newlines() {
592 _osh-parse-error '
593 for
594 do
595 done
596 '
597
598 _osh-parse-error '
599 case
600 in esac
601 '
602
603 _osh-parse-error '
604 while
605 do
606 done
607 '
608
609 _osh-parse-error '
610 if
611 then
612 fi
613 '
614
615 _osh-parse-error '
616 if true
617 then
618 elif
619 then
620 fi
621 '
622
623 _osh-parse-error '
624 case |
625 in
626 esac
627 '
628
629 _osh-parse-error '
630 case ;
631 in
632 esac
633 '
634
635 _osh-should-parse '
636 if
637 true
638 then
639 fi
640 '
641
642 _osh-should-parse '
643 while
644 false
645 do
646 done
647 '
648
649 _osh-should-parse '
650 while
651 true;
652 false
653 do
654 done
655 '
656
657 _osh-should-parse '
658 if true
659 then
660 fi
661 '
662
663 _osh-should-parse '
664 while true;
665 false
666 do
667 done
668 '
669}
670
671test-no-parse-backticks() {
672
673 # These are allowed
674 _osh-should-parse 'echo `echo hi`'
675 _osh-should-parse 'echo "foo = `echo hi`"'
676
677 _assert-status-2 -o no_parse_backticks -n -c 'echo `echo hi`'
678 _assert-status-2 -o no_parse_backticks -n -c 'echo "foo = `echo hi`"'
679}
680
681test-shell_for() {
682
683 _osh-parse-error 'for x in &'
684
685 _osh-parse-error 'for (( i=0; i<10; i++ )) ls'
686
687 # ( is invalid
688 _osh-parse-error 'for ( i=0; i<10; i++ )'
689
690 _osh-parse-error 'for $x in 1 2 3; do echo $i; done'
691 _osh-parse-error 'for x.y in 1 2 3; do echo $i; done'
692 _osh-parse-error 'for x in 1 2 3; &'
693 _osh-parse-error 'for foo BAD'
694
695 # BUG fix: var is a valid name
696 _osh-should-parse 'for var in x; do echo $var; done'
697}
698
699#
700# Different source_t variants
701#
702
703test-nested_source_argvword() {
704 # source.ArgvWord
705 _runtime-parse-error '
706 code="printf % x"
707 eval $code
708 '
709}
710
711test-eval_parse_error() {
712 _runtime-parse-error '
713 x="echo )"
714 eval $x
715 '
716}
717
718trap_parse_error() {
719 _runtime-parse-error '
720 trap "echo )" EXIT
721 '
722}
723
724test-proc-func-reserved() {
725 ### Prevents confusion
726
727 _osh-parse-error 'proc p (x) { echo hi }'
728 _osh-parse-error 'func f (x) { return (x) }'
729
730 # In expression mode, reserved for
731 # var x = proc (x; y) ^( echo hi )
732 _osh-parse-error '= func'
733 _osh-parse-error '= proc'
734}
735
736# Cases in their own file
737cases-in-files() {
738 for test_file in test/parse-errors/*.sh; do
739 case-banner "FILE $test_file"
740
741 set +o errexit
742 $OSH $test_file
743 local status=$?
744 set -o errexit
745
746 if test -z "${SH_ASSERT_DISABLE:-}"; then
747 if test $status != 2; then
748 die "Expected status 2 from parse error file, got $status"
749 fi
750 fi
751 done
752}
753
754test-case() {
755 readonly -a YES=(
756 # Right is optional
757 'case $x in foo) echo
758esac'
759 'case $x in foo) echo ;; esac'
760 'case $x in foo) echo ;& esac'
761 'case $x in foo) echo ;;& esac'
762 )
763
764 readonly -a NO=(
765 ';&'
766 'echo ;&'
767 'echo ;;&'
768 )
769
770 for c in "${YES[@]}"; do
771 echo "--- test-case YES $c"
772
773 _osh-should-parse "$c"
774 echo
775
776 bash -n -c "$c"
777 echo bash=$?
778 done
779
780 for c in "${NO[@]}"; do
781 echo "--- test-case NO $c"
782
783 _osh-parse-error "$c"
784
785 set +o errexit
786 bash -n -c "$c"
787 echo bash=$?
788 set -o errexit
789 done
790}
791
792test-ysh-interaction() {
793 # remove special parsing
794 _osh-parse-error '= \xff'
795}
796
797all() {
798 section-banner 'Cases in Files'
799
800 cases-in-files
801
802 section-banner 'Cases in Functions, with strings'
803
804 run-test-funcs
805}
806
807# TODO: Something like test/parse-err-compare.sh
808
809all-with-bash() {
810 # override OSH and YSH
811 SH_ASSERT_DISABLE=1 OSH=bash YSH=bash all
812}
813
814all-with-dash() {
815 # override OSH and YSH
816 SH_ASSERT_DISABLE=1 OSH=dash YSH=dash all
817}
818
819soil-run-py() {
820 ### Run in CI with Python
821
822 # output _tmp/other/parse-errors.txt
823
824 all
825}
826
827soil-run-cpp() {
828 ninja _bin/cxx-asan/osh
829 OSH=_bin/cxx-asan/osh all
830}
831
832release-oils-for-unix() {
833 readonly OILS_VERSION=$(head -n 1 oils-version.txt)
834 local dir="../benchmark-data/src/oils-for-unix-$OILS_VERSION"
835
836 # Maybe rebuild it
837 pushd $dir
838 _build/oils.sh --skip-rebuild
839 popd
840
841 local suite_name=parse-errors-osh-cpp
842 OSH=$dir/_bin/cxx-opt-sh/osh \
843 run-other-suite-for-release $suite_name all
844}
845
846run-for-release() {
847 ### Test with bin/osh and the ASAN binary.
848
849 run-other-suite-for-release parse-errors all
850
851 release-oils-for-unix
852}
853
854"$@"