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

849 lines, 413 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-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 # Disable Oil stuff for osh_{parse,eval}.asan
178 if false; then
179 # Non-standard arith sub $[1 + 2]
180 _osh-parse-error 'echo $[ 1 + 2 ;'
181
182 # What's going on here? No location info?
183 _osh-parse-error 'echo $[ 1 + 2 /'
184
185 _osh-parse-error 'echo $[ 1 + 2 / 3'
186 _osh-parse-error 'echo $['
187 fi
188
189 # Should be an error
190 _osh-parse-error 'a[x+]=1'
191
192 # Check what happens when you wrap
193 # This could use more detail - it needs the eval location
194 _osh-error-2 'eval a[x+]=1'
195
196 _osh-parse-error 'a[]=1'
197
198 _osh-parse-error 'a[*]=1'
199
200 # These errors are different because the arithmetic lexer mode has } but not
201 # {. May be changed later.
202 _osh-parse-error '(( a + { ))'
203 _osh-parse-error '(( a + } ))'
204
205}
206
207test-arith-integration() {
208 # Regression: these were not parse errors, but should be!
209 _osh-parse-error 'echo $((a b))'
210 _osh-parse-error '((a b))'
211
212 # Empty arithmetic expressions
213 _osh-should-parse 'for ((x=0; x<5; x++)); do echo $x; done'
214 _osh-should-parse 'for ((; x<5; x++)); do echo $x; done'
215 _osh-should-parse 'for ((; ; x++)); do echo $x; done'
216 _osh-should-parse 'for ((; ;)); do echo $x; done'
217
218 # Extra tokens on the end of each expression
219 _osh-parse-error 'for ((x=0; x<5; x++ b)); do echo $x; done'
220
221 _osh-parse-error 'for ((x=0 b; x<5; x++)); do echo $x; done'
222 _osh-parse-error 'for ((x=0; x<5 b; x++)); do echo $x; done'
223
224 _osh-parse-error '${a:1+2 b}'
225 _osh-parse-error '${a:1+2:3+4 b}'
226
227 _osh-parse-error '${a[1+2 b]}'
228}
229
230test-arith-expr() {
231 # BUG: the token is off here
232 _osh-parse-error '$(( 1 + + ))'
233
234 # BUG: not a great error either
235 _osh-parse-error '$(( 1 2 ))'
236
237 # Triggered a crash!
238 _osh-parse-error '$(( - ; ))'
239
240 # NOTE: This is confusing, should point to ` for command context?
241 _osh-parse-error '$(( ` ))'
242
243 _osh-parse-error '$(( $ ))'
244
245 # Invalid assignments
246 _osh-parse-error '$(( x+1 = 42 ))'
247 _osh-parse-error '$(( (x+42)++ ))'
248 _osh-parse-error '$(( ++(x+42) ))'
249
250 # Note these aren't caught because '1' is an ArithWord like 0x$x
251 #_osh-parse-error '$(( 1 = foo ))'
252 #_osh-parse-error '$(( 1++ ))'
253 #_osh-parse-error '$(( ++1 ))'
254}
255
256test-command-sub() {
257 _osh-parse-error '
258 echo line 2
259 echo $( echo '
260 _osh-parse-error '
261 echo line 2
262 echo ` echo '
263
264 # This is source.Reparsed('backticks', ...)
265
266 # Both unclosed
267 _osh-parse-error '
268 echo line 2
269 echo ` echo \` '
270
271 # Only the inner one is unclosed
272 _osh-parse-error '
273 echo line 2
274 echo ` echo \`unclosed ` '
275
276 _osh-parse-error 'echo `for x in`'
277}
278
279test-bool-expr() {
280 # Extra word
281 _osh-parse-error '[[ a b ]]'
282 _osh-parse-error '[[ a "a"$(echo hi)"b" ]]'
283
284 # Wrong error message
285 _osh-parse-error '[[ a == ]]'
286
287 if false; then
288 # Invalid regex
289 # These are currently only detected at runtime.
290 _osh-parse-error '[[ $var =~ * ]]'
291 _osh-parse-error '[[ $var =~ + ]]'
292 fi
293
294 # Unbalanced parens
295 _osh-parse-error '[[ ( 1 == 2 - ]]'
296
297 _osh-parse-error '[[ == ]]'
298 _osh-parse-error '[[ ) ]]'
299 _osh-parse-error '[[ ( ]]'
300
301 _osh-parse-error '[[ ;;; ]]'
302 _osh-parse-error '[['
303
304 # Expected right )
305 _osh-parse-error '[[ ( a == b foo${var} ]]'
306}
307
308test-regex-nix() {
309 ### Based on Nix bug
310
311 # Nix idiom - added space
312 _osh-should-parse '
313if [[ ! (" ${params[*]} " =~ " -shared " || " ${params[*]} " =~ " -static " ) ]]; then
314 echo hi
315fi
316'
317
318 # (x) is part of the regex
319 _osh-should-parse '
320if [[ (foo =~ (x) ) ]]; then
321 echo hi
322fi
323'
324 # Nix idiom - reduced
325 _osh-should-parse '
326if [[ (foo =~ x) ]]; then
327 echo hi
328fi
329'
330
331 # Nix idiom - original
332 _osh-should-parse '
333if [[ ! (" ${params[*]} " =~ " -shared " || " ${params[*]} " =~ " -static ") ]]; then
334 echo hi
335fi
336'
337}
338
339test-regex-pipe() {
340 # Pipe in outer expression - it becomes Lit_Other, which is fine
341
342 # Well we need a special rule for this probably
343 local s='[[ a =~ b|c ]]'
344 bash -n -c "$s"
345 _osh-should-parse "$s"
346}
347
348test-regex-space() {
349 # initial space
350 _osh-should-parse '[[ a =~ ( ) ]]'
351 _osh-should-parse '[[ a =~ (b c) ]]'
352 _osh-should-parse '[[ a =~ (a b)(c d) ]]'
353
354 # Hm bash allows newline inside (), but not outside
355 # I feel like we don't need to duplicate this
356
357 local s='[[ a =~ (b
358c) ]]'
359 bash -n -c "$s"
360 echo bash=$?
361
362 _osh-should-parse "$s"
363}
364
365test-regex-right-paren() {
366 # BashRegex lexer mode
367 _osh-should-parse '[[ a =~ b ]]'
368 _osh-should-parse '[[ a =~ (b) ]]' # this is a regex
369 _osh-should-parse '[[ (a =~ b) ]]' # this is grouping
370 _osh-should-parse '[[ (a =~ (b)) ]]' # regex and grouping!
371
372 _osh-parse-error '[[ (a =~ (' # EOF
373 _osh-parse-error '[[ (a =~ (b' # EOF
374
375 return
376 # Similar thing for extglob
377 _osh-should-parse '[[ a == b ]]'
378 _osh-should-parse '[[ a == @(b) ]]' # this is a regex
379 _osh-should-parse '[[ (a == b) ]]' # this is grouping
380 _osh-should-parse '[[ (a == @(b)) ]]' # regex and grouping!
381}
382
383
384# These don't have any location information.
385test-test-builtin() {
386 # Some of these come from osh/bool_parse.py, and some from
387 # osh/builtin_bracket.py.
388
389 # Extra token
390 _runtime-parse-error '[ x -a y f ]'
391 _runtime-parse-error 'test x -a y f'
392
393 # Missing closing ]
394 _runtime-parse-error '[ x '
395
396 # Hm some of these errors are wonky. Need positions.
397 _runtime-parse-error '[ x x ]'
398
399 _runtime-parse-error '[ x x "a b" ]'
400
401 # This is a runtime error but is handled similarly
402 _runtime-parse-error '[ -t xxx ]'
403
404 _runtime-parse-error '[ \( x -a -y -a z ]'
405
406 # -o tests if an option is enabled.
407 #_osh-parse-error '[ -o x ]'
408}
409
410test-printf-builtin() {
411 _runtime-parse-error 'printf %'
412 _runtime-parse-error 'printf [%Z]'
413
414 _runtime-parse-error 'printf -v "-invalid-" %s foo'
415}
416
417test-other-builtins() {
418 _runtime-parse-error 'shift 1 2'
419 _runtime-parse-error 'shift zzz'
420
421 _runtime-parse-error 'pushd x y'
422 _runtime-parse-error 'pwd -x'
423
424 _runtime-parse-error 'pp x foo a-x'
425
426 _runtime-parse-error 'wait zzz'
427 _runtime-parse-error 'wait %jobspec-not-supported'
428
429 _runtime-parse-error 'unset invalid-var-name'
430 _runtime-parse-error 'getopts 'hc:' invalid-var-name'
431}
432
433test-quoted-strings() {
434 _osh-parse-error '"unterminated double'
435
436 _osh-parse-error "'unterminated single"
437
438 _osh-parse-error '
439 "unterminated double multiline
440 line 1
441 line 2'
442
443 _osh-parse-error "
444 'unterminated single multiline
445 line 1
446 line 2"
447}
448
449test-braced-var-sub() {
450 # These should have ! for a prefix query
451 _osh-parse-error 'echo ${x*}'
452 _osh-parse-error 'echo ${x@}'
453
454 _osh-parse-error 'echo ${x.}'
455}
456
457test-cmd-parse() {
458 _osh-parse-error 'FOO=1 break'
459 _osh-parse-error 'break 1 2'
460
461 _osh-parse-error 'x"y"() { echo hi; }'
462
463 _osh-parse-error 'function x"y" { echo hi; }'
464
465 _osh-parse-error '}'
466
467 _osh-parse-error 'case foo in *) echo '
468 _osh-parse-error 'case foo in x|) echo '
469
470 _osh-parse-error 'ls foo|'
471 _osh-parse-error 'ls foo&&'
472
473 _osh-parse-error 'foo()'
474
475 # parse_ignored
476 _osh-should-parse 'break >out'
477 _ysh-parse-error 'break >out'
478
479 # Unquoted (
480 _osh-parse-error '[ ( x ]'
481}
482
483test-append() {
484 # from spec/test-append.test.sh. bash treats this as a runtime error, but it's a
485 # parse error in OSH.
486 _osh-parse-error 'a[-1]+=(4 5)'
487}
488
489test-redirect() {
490 _osh-parse-error 'echo < <<'
491 _osh-parse-error 'echo $( echo > >> )'
492}
493
494test-simple-command() {
495 _osh-parse-error 'PYTHONPATH=. FOO=(1 2) python'
496 # not statically detected after dynamic assignment
497 #_osh-parse-error 'echo foo FOO=(1 2)'
498
499 _osh-parse-error 'PYTHONPATH+=1 python'
500
501 # Space sensitivity: disallow =
502 _osh-parse-error '=var'
503 _osh-parse-error '=f(x)'
504
505 _ysh-parse-error '=var'
506 _ysh-parse-error '=f(x)'
507}
508
509# Old code? All these pass
510DISABLED-assign() {
511 _osh-parse-error 'local name$x'
512 _osh-parse-error 'local "ab"'
513 _osh-parse-error 'local a.b'
514
515 _osh-parse-error 'FOO=1 local foo=1'
516}
517
518# I can't think of any other here doc error conditions except arith/var/command
519# substitution, and unterminated.
520test-here-doc() {
521 # Arith in here doc
522 _osh-parse-error 'cat <<EOF
523$(( 1 * ))
524EOF
525'
526
527 # Varsub in here doc
528 _osh-parse-error 'cat <<EOF
529invalid: ${a!}
530EOF
531'
532
533 _osh-parse-error 'cat <<EOF
534$(for x in )
535EOF
536'
537}
538
539test-here-doc-delimiter() {
540 # NOTE: This is more like the case where.
541 _osh-parse-error 'cat << $(invalid here end)'
542
543 # TODO: Arith parser doesn't have location information
544 _osh-parse-error 'cat << $((1+2))'
545 _osh-parse-error 'cat << a=(1 2 3)'
546 _osh-parse-error 'cat << \a$(invalid)'
547
548 # Actually the $invalid part should be highlighted... yeah an individual
549 # part is the problem.
550 #"cat << 'single'$(invalid)"
551 _osh-parse-error 'cat << "double"$(invalid)'
552 _osh-parse-error 'cat << ~foo/$(invalid)'
553 _osh-parse-error 'cat << $var/$(invalid)'
554}
555
556test-args-parse-builtin() {
557 _runtime-parse-error 'read -x' # invalid
558 _runtime-parse-error 'builtin read -x' # ditto
559
560 _runtime-parse-error 'read -n' # expected argument for -n
561 _runtime-parse-error 'read -n x' # expected integer
562
563 _runtime-parse-error 'set -o errexit +o oops'
564
565 # not implemented yet
566 #_osh-parse-error 'read -t x' # expected floating point number
567
568 # TODO:
569 # - invalid choice
570 # - Oil flags: invalid long flag, boolean argument, etc.
571}
572
573test-args-parse-more() {
574 _runtime-parse-error 'set -z'
575 _runtime-parse-error 'shopt -s foo'
576 _runtime-parse-error 'shopt -z'
577}
578
579DISABLED-args-parse-main() {
580 $OSH --ast-format x
581
582 $OSH -o errexit +o oops
583}
584
585test-invalid-brace-ranges() {
586 _osh-parse-error 'echo {1..3..-1}'
587 _osh-parse-error 'echo {1..3..0}'
588 _osh-parse-error 'echo {3..1..1}'
589 _osh-parse-error 'echo {3..1..0}'
590 _osh-parse-error 'echo {a..Z}'
591 _osh-parse-error 'echo {a..z..0}'
592 _osh-parse-error 'echo {a..z..-1}'
593 _osh-parse-error 'echo {z..a..1}'
594}
595
596test-extra-newlines() {
597 _osh-parse-error '
598 for
599 do
600 done
601 '
602
603 _osh-parse-error '
604 case
605 in esac
606 '
607
608 _osh-parse-error '
609 while
610 do
611 done
612 '
613
614 _osh-parse-error '
615 if
616 then
617 fi
618 '
619
620 _osh-parse-error '
621 if true
622 then
623 elif
624 then
625 fi
626 '
627
628 _osh-parse-error '
629 case |
630 in
631 esac
632 '
633
634 _osh-parse-error '
635 case ;
636 in
637 esac
638 '
639
640 _osh-should-parse '
641 if
642 true
643 then
644 fi
645 '
646
647 _osh-should-parse '
648 while
649 false
650 do
651 done
652 '
653
654 _osh-should-parse '
655 while
656 true;
657 false
658 do
659 done
660 '
661
662 _osh-should-parse '
663 if true
664 then
665 fi
666 '
667
668 _osh-should-parse '
669 while true;
670 false
671 do
672 done
673 '
674}
675
676test-parse_backticks() {
677
678 # These are allowed
679 _osh-should-parse 'echo `echo hi`'
680 _osh-should-parse 'echo "foo = `echo hi`"'
681
682 _assert-status-2 +O test-parse_backticks -n -c 'echo `echo hi`'
683 _assert-status-2 +O test-parse_backticks -n -c 'echo "foo = `echo hi`"'
684}
685
686test-shell_for() {
687
688 _osh-parse-error 'for x in &'
689
690 _osh-parse-error 'for (( i=0; i<10; i++ )) ls'
691
692 # ( is invalid
693 _osh-parse-error 'for ( i=0; i<10; i++ )'
694
695 _osh-parse-error 'for $x in 1 2 3; do echo $i; done'
696 _osh-parse-error 'for x.y in 1 2 3; do echo $i; done'
697 _osh-parse-error 'for x in 1 2 3; &'
698 _osh-parse-error 'for foo BAD'
699
700 # BUG fix: var is a valid name
701 _osh-should-parse 'for var in x; do echo $var; done'
702}
703
704#
705# Different source_t variants
706#
707
708test-nested_source_argvword() {
709 # source.ArgvWord
710 _runtime-parse-error '
711 code="printf % x"
712 eval $code
713 '
714}
715
716test-eval_parse_error() {
717 _runtime-parse-error '
718 x="echo )"
719 eval $x
720 '
721}
722
723trap_parse_error() {
724 _runtime-parse-error '
725 trap "echo )" EXIT
726 '
727}
728
729test-proc_func_reserved() {
730 ### Prevents confusion
731
732 _osh-parse-error 'proc p (x) { echo hi }'
733 _osh-parse-error 'func f (x) { return (x) }'
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
792all() {
793 section-banner 'Cases in Files'
794
795 cases-in-files
796
797 section-banner 'Cases in Functions, with strings'
798
799 run-test-funcs
800}
801
802# TODO: Something like test/parse-err-compare.sh
803
804all-with-bash() {
805 # override OSH and YSH
806 SH_ASSERT_DISABLE=1 OSH=bash YSH=bash all
807}
808
809all-with-dash() {
810 # override OSH and YSH
811 SH_ASSERT_DISABLE=1 OSH=dash YSH=dash all
812}
813
814soil-run-py() {
815 ### Run in CI with Python
816
817 # output _tmp/other/parse-errors.txt
818
819 all
820}
821
822soil-run-cpp() {
823 ninja _bin/cxx-asan/osh
824 OSH=_bin/cxx-asan/osh all
825}
826
827release-oils-for-unix() {
828 readonly OIL_VERSION=$(head -n 1 oils-version.txt)
829 local dir="../benchmark-data/src/oils-for-unix-$OIL_VERSION"
830
831 # Maybe rebuild it
832 pushd $dir
833 _build/oils.sh --skip-rebuild
834 popd
835
836 local suite_name=parse-errors-osh-cpp
837 OSH=$dir/_bin/cxx-opt-sh/osh \
838 run-other-suite-for-release $suite_name all
839}
840
841run-for-release() {
842 ### Test with bin/osh and the ASAN binary.
843
844 run-other-suite-for-release parse-errors all
845
846 release-oils-for-unix
847}
848
849"$@"