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

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