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

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