OILS / test / parse-errors.sh View on Github | oilshell.org

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