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

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