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

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