1 | #!/usr/bin/env bash
|
2 | #
|
3 | # A file that tickles many runtime errors, to see the error message.
|
4 | # Sometimes spec tests are better - 'test/spec.sh smoke -v -d' can also show
|
5 | # errors clearly.
|
6 | #
|
7 | # Usage:
|
8 | # test/runtime-errors.sh <function name>
|
9 | #
|
10 | # TODO: Migrate the unquoted-* functions below to test-* functions, which means
|
11 | # adding assertions.
|
12 |
|
13 | source test/common.sh
|
14 | source test/sh-assert.sh # banner, _assert-sh-status
|
15 |
|
16 | # Note: should run in bash/dash mode, where we don't check errors
|
17 | OSH=${OSH:-bin/osh}
|
18 | YSH=${YSH:-bin/ysh}
|
19 |
|
20 | _run-test-func() {
|
21 | ### Run a function, and optionally assert status
|
22 |
|
23 | local test_func=$1
|
24 | local expected_status=${2:-}
|
25 |
|
26 | echo
|
27 | echo "===== TEST function: $test_func ====="
|
28 | echo
|
29 |
|
30 | $OSH $0 $test_func
|
31 |
|
32 | status=$?
|
33 | if test -n "$expected_status"; then
|
34 | if test $status != $expected_status; then
|
35 | die "*** Test $test_func -> status $status, expected $expected_status"
|
36 | fi
|
37 | fi
|
38 |
|
39 | echo "----- STATUS: $?"
|
40 | echo
|
41 | }
|
42 |
|
43 | test-FAIL() {
|
44 | ### Make sure the assertions work
|
45 |
|
46 | # Error string
|
47 | _osh-error-1 'echo hi > /zzz'
|
48 |
|
49 | return
|
50 |
|
51 | # doesn't fail
|
52 | _osh-error-2 'echo hi'
|
53 |
|
54 | echo nope
|
55 | }
|
56 |
|
57 | #
|
58 | # PARSE ERRORS
|
59 | #
|
60 |
|
61 | test-source_bad_syntax() {
|
62 | cat >_tmp/bad-syntax.sh <<EOF
|
63 | if foo; echo ls; fi
|
64 | EOF
|
65 | _osh-error-2 '. _tmp/bad-syntax.sh'
|
66 | }
|
67 |
|
68 | # NOTE:
|
69 | # - bash correctly reports line 25 (24 would be better)
|
70 | # - mksh: no line number
|
71 | # - zsh: line 2 of eval, which doesn't really help.
|
72 | # - dash: ditto, line 2 of eval
|
73 | test-eval_bad_syntax() {
|
74 | _osh-error-2 '
|
75 | code="if foo; echo ls; fi"
|
76 | eval "echo --
|
77 | $code"
|
78 | '
|
79 | }
|
80 |
|
81 | #
|
82 | # COMMAND ERRORS
|
83 | #
|
84 |
|
85 | test-no_such_command() {
|
86 | _osh-error-X 127 'set -o errexit; ZZZZZ; echo UNREACHABLE'
|
87 | }
|
88 |
|
89 | test-no_such_command_commandsub() {
|
90 | _osh-should-run 'set -o errexit; echo $(ZZZZZ); echo UNREACHABLE'
|
91 | _osh-error-X 127 'set -o errexit; shopt -s command_sub_errexit; echo $(ZZZZZ); echo UNREACHABLE'
|
92 | }
|
93 |
|
94 | unquoted-no_such_command_heredoc() {
|
95 | set -o errexit
|
96 |
|
97 | # Note: bash gives the line of the beginning of the here doc! Not the actual
|
98 | # line.
|
99 | cat <<EOF
|
100 | one
|
101 | $(ZZZZZ)
|
102 | three
|
103 | EOF
|
104 | echo 'SHOULD NOT GET HERE'
|
105 | }
|
106 |
|
107 | test-failed_command() {
|
108 | _osh-error-1 'set -o errexit; false; echo UNREACHABLE'
|
109 | }
|
110 |
|
111 | # This quotes the same line of code twice, but maybe that's OK. At least there
|
112 | # is different column information.
|
113 | test-errexit_usage_error() {
|
114 | _osh-error-2 'set -o errexit; type -z'
|
115 | }
|
116 |
|
117 | test-errexit_subshell() {
|
118 | # Note: for loops, while loops don't trigger errexit; their components do
|
119 | _osh-error-X 42 'set -o errexit; ( echo subshell; exit 42; )'
|
120 | }
|
121 |
|
122 | TODO-BUG-test-errexit_pipeline() {
|
123 | # We don't blame the right location here
|
124 |
|
125 | # BUG: what happnened here? Is there a race?
|
126 | local code='set -o errexit; set -o pipefail; echo subshell | cat | exit 42 | wc -l'
|
127 |
|
128 | #local code='set -o errexit; set -o pipefail; echo subshell | cat | exit 42'
|
129 |
|
130 | bash -c "$code"
|
131 | echo status=$?
|
132 |
|
133 | _osh-error-X 42 "$code"
|
134 | }
|
135 |
|
136 | test-errexit_dbracket() {
|
137 | _osh-error-1 'set -o errexit; [[ -n "" ]]; echo UNREACHABLE'
|
138 | }
|
139 |
|
140 | shopt -s expand_aliases
|
141 | # Why can't this be in the function?
|
142 | alias foo='echo hi; ls '
|
143 |
|
144 | test-errexit_alias() {
|
145 | _osh-error-1 'set -o errexit; type foo; foo /nonexistent'
|
146 | }
|
147 |
|
148 | _sep() {
|
149 | echo
|
150 | echo '---------------------------'
|
151 | }
|
152 |
|
153 | test-command-not-found() {
|
154 | _osh-error-X 127 'findz'
|
155 | }
|
156 |
|
157 | test-errexit-one-process() {
|
158 | # two quotations of same location: not found then errexit
|
159 | _ysh-error-X 127 'zz'
|
160 |
|
161 | _sep
|
162 |
|
163 | # two quotations, different location
|
164 | _ysh-error-1 'echo hi > ""'
|
165 |
|
166 | _sep
|
167 |
|
168 | _ysh-error-1 'shopt -s failglob; echo *.ZZZZ'
|
169 |
|
170 | _sep
|
171 |
|
172 | _ysh-error-1 'cd /x'
|
173 |
|
174 | _sep
|
175 |
|
176 | # TODO: remove duplicate snippet
|
177 | _ysh-error-X 126 './README.md; echo hi'
|
178 |
|
179 | # one location
|
180 | _ysh-error-2 'ls /x; echo $?'
|
181 |
|
182 | _sep
|
183 |
|
184 | _ysh-error-2 'declare cmd=ls; $cmd /x; echo $?'
|
185 |
|
186 | _sep
|
187 |
|
188 | # one location
|
189 | _ysh-error-1 'echo $undef'
|
190 |
|
191 | _sep
|
192 |
|
193 | # Show multiple "nested" errors, and then errexit
|
194 | _osh-error-1 '
|
195 | eval "("
|
196 | echo status=$?
|
197 |
|
198 | eval ")"
|
199 | echo status=$?
|
200 |
|
201 | set -e; shopt -s verbose_errexit
|
202 | false
|
203 | echo DONE
|
204 | '
|
205 |
|
206 | _sep
|
207 | _osh-error-1 'shopt --set ysh:upgrade; [[ 0 -eq 1 ]]'
|
208 |
|
209 | # not parsed
|
210 | _ysh-error-2 '[[ 0 -eq 1 ]]'
|
211 |
|
212 | _sep
|
213 |
|
214 | _osh-error-1 'shopt --set ysh:upgrade; (( 0 ))'
|
215 |
|
216 | # not parsed
|
217 | _ysh-error-2 '(( 0 ))'
|
218 | }
|
219 |
|
220 | test-errexit-multiple-processes() {
|
221 | ### A representative set of errors. For consolidating code quotations
|
222 |
|
223 | # command_sub_errexit. Hm this gives 2 errors as well, because of inherit_errexit
|
224 | _ysh-error-1 'echo t=$(true) f=$(false; true)'
|
225 | #return
|
226 |
|
227 | _sep
|
228 |
|
229 | # BUG introduced by shopt -s no_last_fork: Even though set -o pipefail is on
|
230 | # in YSH, the entire shell does NOT exit!
|
231 | #
|
232 | # This is because 'wc -l' does exec. And then there is nothing to "modify"
|
233 | # the exit status based on pipefail.
|
234 | #
|
235 | # So it's actually unsound to do this optmization when set -o pipefail is on.
|
236 | # Combined with shopt -s lastpipe
|
237 |
|
238 | #_ysh-should-run 'ls | false | wc -l'
|
239 | _ysh-error-1 'ls | false | wc -l'
|
240 |
|
241 | _sep
|
242 |
|
243 | # note: need trailing echo to prevent pipeline optimization
|
244 | _ysh-error-X 42 'ls | { echo hi; ( exit 42 ); } | wc -l; echo'
|
245 |
|
246 | _sep
|
247 |
|
248 | # Showing errors for THREE PIDs here! That is technically correct, but
|
249 | # noisy.
|
250 | _ysh-error-1 '{ echo one; ( exit 42 ); } |\
|
251 | { false; wc -l; }'
|
252 |
|
253 | _sep
|
254 |
|
255 | # realistic example
|
256 | _ysh-error-1 '{ ls; false; } \
|
257 | | wc -l
|
258 | '
|
259 |
|
260 | _sep
|
261 |
|
262 | # Three errors!
|
263 | _ysh-error-1 '{ ls; ( false; true ); } | wc -l; echo hi'
|
264 |
|
265 | _sep
|
266 |
|
267 | _ysh-error-X 127 'ls <(sort YY) <(zz); echo hi'
|
268 |
|
269 | # 2 kinds of errors
|
270 | _sep
|
271 | _ysh-error-X 127 'zz <(sort YY) <(sort ZZ); echo hi'
|
272 |
|
273 | # This one has badly interleaved errors!
|
274 | _sep
|
275 | _ysh-error-X 127 'yy | zz'
|
276 |
|
277 | _sep
|
278 | _osh-error-1 'shopt -s ysh:upgrade; echo $([[ 0 -eq 1 ]])'
|
279 |
|
280 | _sep
|
281 | _osh-error-1 'shopt -s ysh:upgrade; var y = $([[ 0 -eq 1 ]])'
|
282 | }
|
283 |
|
284 |
|
285 | _strict-errexit-case() {
|
286 | local code=$1
|
287 |
|
288 | #case-banner "[strict_errexit] $code"
|
289 |
|
290 | _osh-error-1 \
|
291 | "set -o errexit; shopt -s strict_errexit; $code"
|
292 | echo
|
293 | }
|
294 |
|
295 | test-strict-errexit-1() {
|
296 | # Test out all the location info
|
297 |
|
298 | _strict-errexit-case '! { echo 1; echo 2; }'
|
299 |
|
300 | _strict-errexit-case '{ echo 1; echo 2; } && true'
|
301 | _strict-errexit-case '{ echo 1; echo 2; } || true'
|
302 | _strict-errexit-case '{ echo 1; echo 2; } >/dev/null || true'
|
303 |
|
304 | # More chains
|
305 | _strict-errexit-case '{ echo 1; echo 2; } && true && true'
|
306 | _strict-errexit-case 'true && { echo 1; echo 2; } || true || true'
|
307 | _strict-errexit-case 'true && true && { echo 1; echo 2; } || true || true'
|
308 |
|
309 | _strict-errexit-case 'if { echo 1; echo 2; }; then echo IF; fi'
|
310 | _strict-errexit-case 'if { echo 1; echo 2; } >/dev/null; then echo IF; fi'
|
311 |
|
312 | _strict-errexit-case 'while { echo 1; echo 2; }; do echo WHILE; done'
|
313 | _strict-errexit-case 'until { echo 1; echo 2; }; do echo UNTIL; done'
|
314 |
|
315 | # Must be separate lines for parsing options to take effect
|
316 | _strict-errexit-case 'shopt -s ysh:upgrade
|
317 | proc p { echo p }
|
318 | if p { echo hi }'
|
319 | }
|
320 |
|
321 | test-strict-errexit-conditionals() {
|
322 | # this works, even though this is a subshell
|
323 | _strict-errexit-case '
|
324 | myfunc() { return 1; }
|
325 |
|
326 | while ( myfunc )
|
327 | do
|
328 | echo yes
|
329 | done
|
330 | '
|
331 |
|
332 | # Conditional - Proc - Child Interpreter Problem (command sub)
|
333 | # Same problem here. A proc run in a command sub LOSES the exit code.
|
334 | _strict-errexit-case '
|
335 | myfunc() { return 1; }
|
336 |
|
337 | while test "$(myfunc)" != ""
|
338 | do
|
339 | echo yes
|
340 | done
|
341 | '
|
342 |
|
343 | # Process Sub is be disallowed; it could invoke a proc!
|
344 | _strict-errexit-case '
|
345 | myfunc() { return 1; }
|
346 |
|
347 | if cat <(ls)
|
348 | then
|
349 | echo yes
|
350 | fi
|
351 | '
|
352 |
|
353 | # Conditional - Proc - Child Interpreter Problem (pipeline)
|
354 | _strict-errexit-case '
|
355 | myfunc() {
|
356 | return 1
|
357 | }
|
358 |
|
359 | set -o pipefail
|
360 | while myfunc | cat
|
361 | do
|
362 | echo yes
|
363 | done
|
364 | '
|
365 |
|
366 | # regression for issue #1107 bad error message
|
367 | # Also revealed #1113: the strict_errexit check was handled inside the
|
368 | # command sub process!
|
369 | _strict-errexit-case '
|
370 | myfunc() {
|
371 | return 1
|
372 | }
|
373 |
|
374 | foo=$(true)
|
375 |
|
376 | # test assignment without proc
|
377 | while bar=$(false)
|
378 | do
|
379 | echo yes
|
380 | done
|
381 |
|
382 | # issue 1007 was caused using command.ShAssignment, rather than the more common
|
383 | # command.Sentence with ;
|
384 | while spam=$(myfunc)
|
385 | do
|
386 | echo yes
|
387 | done
|
388 | '
|
389 | }
|
390 |
|
391 | # OLD WAY OF BLAMING
|
392 | # Note: most of these don't fail
|
393 | test-strict-errexit-old() {
|
394 | # Test out all the location info
|
395 |
|
396 | # command.Pipeline.
|
397 | _strict-errexit-case 'if ls | wc -l; then echo Pipeline; fi'
|
398 | _strict-errexit-case 'if ! ls | wc -l; then echo failed; fi'
|
399 |
|
400 | # This one is ALLOWED
|
401 | #_strict-errexit-case 'if ! ls; then echo Pipeline; fi'
|
402 |
|
403 | # command.AndOr
|
404 | #_strict-errexit-case 'if echo a && echo b; then echo AndOr; fi'
|
405 |
|
406 | # command.DoGroup
|
407 | _strict-errexit-case '! for x in a; do echo $x; done'
|
408 |
|
409 | # command.BraceGroup
|
410 | _strict-errexit-case '_func() { echo; }; ! _func'
|
411 | _strict-errexit-case '! { echo brace; }; echo "should not get here"'
|
412 |
|
413 | # command.Subshell
|
414 | _strict-errexit-case '! ( echo subshell ); echo "should not get here"'
|
415 |
|
416 | # command.WhileUntil
|
417 | _strict-errexit-case '! while false; do echo while; done; echo "should not get here"'
|
418 |
|
419 | # command.If
|
420 | _strict-errexit-case '! if true; then false; fi; echo "should not get here"'
|
421 |
|
422 | # command.Case
|
423 | _strict-errexit-case '! case x in x) echo x;; esac; echo "should not get here"'
|
424 |
|
425 | # command.TimeBlock
|
426 | _strict-errexit-case '! time echo hi; echo "should not get here"'
|
427 |
|
428 | # Command Sub
|
429 | _strict-errexit-case '! echo $(echo hi); echo "should not get here"'
|
430 | }
|
431 |
|
432 | unquoted-pipefail() {
|
433 | false | wc -l
|
434 |
|
435 | set -o errexit
|
436 | set -o pipefail
|
437 | false | wc -l
|
438 |
|
439 | echo 'SHOULD NOT GET HERE'
|
440 | }
|
441 |
|
442 | unquoted-pipefail_no_words() {
|
443 | set -o errexit
|
444 | set -o pipefail
|
445 |
|
446 | # Make sure we can blame this
|
447 | seq 3 | wc -l | > /nonexistent
|
448 |
|
449 | echo done
|
450 | }
|
451 |
|
452 | unquoted-pipefail_func() {
|
453 | set -o errexit -o pipefail
|
454 | f42() {
|
455 | cat
|
456 | # NOTE: If you call 'exit 42', there is no error message displayed!
|
457 | #exit 42
|
458 | return 42
|
459 | }
|
460 |
|
461 | # TODO: blame the right location
|
462 | echo hi | cat | f42 | wc
|
463 |
|
464 | echo 'SHOULD NOT GET HERE'
|
465 | }
|
466 |
|
467 | # TODO: point to {. It's the same sas a subshell so you don't know exactly
|
468 | # which command failed.
|
469 | unquoted-pipefail_group() {
|
470 | set -o errexit -o pipefail
|
471 | echo hi | { cat; sh -c 'exit 42'; } | wc
|
472 |
|
473 | echo 'SHOULD NOT GET HERE'
|
474 | }
|
475 |
|
476 | # TODO: point to (
|
477 | unquoted-pipefail_subshell() {
|
478 | set -o errexit -o pipefail
|
479 | echo hi | (cat; sh -c 'exit 42') | wc
|
480 |
|
481 | echo 'SHOULD NOT GET HERE'
|
482 | }
|
483 |
|
484 | # TODO: point to 'while'
|
485 | unquoted-pipefail_while() {
|
486 | set -o errexit -o pipefail
|
487 | seq 3 | while true; do
|
488 | read line
|
489 | echo X $line X
|
490 | if test "$line" = 2; then
|
491 | sh -c 'exit 42'
|
492 | fi
|
493 | done | wc
|
494 |
|
495 | echo 'SHOULD NOT GET HERE'
|
496 | }
|
497 |
|
498 | # Multiple errors from multiple processes
|
499 | # TODO: These errors get interleaved and messed up. Maybe we should always
|
500 | # print a single line from pipeline processes? We should set their
|
501 | # ErrorFormatter?
|
502 | unquoted-pipefail_multiple() {
|
503 | set -o errexit -o pipefail
|
504 | { echo 'four'; sh -c 'exit 4'; } |
|
505 | { echo 'five'; sh -c 'exit 5'; } |
|
506 | { echo 'six'; sh -c 'exit 6'; }
|
507 | }
|
508 |
|
509 | test-control_flow() {
|
510 | # This prints a WARNING in bash. Not fatal in any shell except zsh.
|
511 | _osh-error-X 0 '
|
512 | break
|
513 | continue
|
514 | echo UNREACHABLE
|
515 | '
|
516 |
|
517 | _osh-error-X 1 '
|
518 | shopt -s strict_control_flow
|
519 | break
|
520 | continue
|
521 | echo UNREACHABLE
|
522 | '
|
523 | }
|
524 |
|
525 | # Errors from core/process.py
|
526 | test-core_process() {
|
527 | _osh-error-1 '
|
528 | echo foo > not/a/file
|
529 | echo foo > /etc/no-perms-for-this
|
530 | '
|
531 |
|
532 | # DISABLED! This messes up the toil log file!
|
533 | # echo hi 1>&3
|
534 | }
|
535 |
|
536 | # Errors from core/state.py
|
537 | test-core-state() {
|
538 |
|
539 | _osh-should-run 'HOME=(a b)'
|
540 |
|
541 | # $HOME is an exported string, so it shouldn't be changed to an array
|
542 | _osh-error-1 'shopt --set strict_array; HOME=(a b)'
|
543 | }
|
544 |
|
545 | unquoted-ambiguous_redirect() {
|
546 | echo foo > "$@"
|
547 | echo 'ambiguous redirect not fatal unless errexit'
|
548 |
|
549 | set -o errexit
|
550 | echo foo > "$@"
|
551 | echo 'should not get here'
|
552 | }
|
553 |
|
554 | # bash semantics.
|
555 | unquoted-ambiguous_redirect_context() {
|
556 | # Problem: A WORD cannot fail. Only a COMMAND can fail.
|
557 |
|
558 | # http://stackoverflow.com/questions/29532904/bash-subshell-errexit-semantics
|
559 | # https://groups.google.com/forum/?fromgroups=#!topic/gnu.bash.bug/NCK_0GmIv2M
|
560 |
|
561 | # http://unix.stackexchange.com/questions/23026/how-can-i-get-bash-to-exit-on-backtick-failure-in-a-similar-way-to-pipefail
|
562 |
|
563 | echo $(echo hi > "$@")
|
564 | echo 'ambiguous is NOT FATAL in command sub'
|
565 | echo
|
566 |
|
567 | foo=$(echo hi > "$@")
|
568 | echo $foo
|
569 | echo 'ambiguous is NOT FATAL in assignment in command sub'
|
570 | echo
|
571 |
|
572 | set -o errexit
|
573 |
|
574 | # This is the issue addressed by command_sub_errexit!
|
575 | echo $(echo hi > "$@")
|
576 | echo 'ambiguous is NOT FATAL in command sub, even if errexit'
|
577 | echo
|
578 |
|
579 | # OK this one works. Because the exit code of the assignment is the exit
|
580 | # code of the RHS?
|
581 | echo 'But when the command sub is in an assignment, it is fatal'
|
582 | foo=$(echo hi > "$@")
|
583 | echo $foo
|
584 | echo 'SHOULD NOT REACH HERE'
|
585 | }
|
586 |
|
587 | unquoted-bad_file_descriptor() {
|
588 | : 1>&7
|
589 | }
|
590 |
|
591 | unquoted-command_sub_errexit() {
|
592 | #set -o errexit
|
593 | shopt -s command_sub_errexit || true
|
594 | shopt -s inherit_errexit || true
|
595 |
|
596 | echo t=$(true) f=$(false) 3=$(exit 3)
|
597 | echo 'SHOULD NOT GET HERE'
|
598 | }
|
599 |
|
600 | unquoted-process_sub_fail() {
|
601 | shopt -s process_sub_fail || true
|
602 | shopt -s inherit_errexit || true
|
603 | set -o errexit
|
604 |
|
605 | cat <(echo a; exit 2) <(echo b; exit 3)
|
606 | echo 'SHOULD NOT GET HERE'
|
607 | }
|
608 |
|
609 | myproc() {
|
610 | echo ---
|
611 | grep pat BAD # exits with code 2
|
612 | #grep pat file.txt
|
613 | echo ---
|
614 | }
|
615 |
|
616 | unquoted-bool_status() {
|
617 | set -o errexit
|
618 |
|
619 | if try --allow-status-01 -- myproc; then
|
620 | echo 'match'
|
621 | else
|
622 | echo 'no match'
|
623 | fi
|
624 | }
|
625 |
|
626 | unquoted-bool_status_simple() {
|
627 | set -o errexit
|
628 |
|
629 | if try --allow-status-01 -- grep pat BAD; then
|
630 | echo 'match'
|
631 | else
|
632 | echo 'no match'
|
633 | fi
|
634 | }
|
635 |
|
636 | #
|
637 | # WORD ERRORS
|
638 | #
|
639 |
|
640 | unquoted-nounset() {
|
641 | set -o nounset
|
642 | echo $x
|
643 |
|
644 | echo 'SHOULD NOT GET HERE'
|
645 | }
|
646 |
|
647 | unquoted-bad_var_ref() {
|
648 | name='bad var name'
|
649 | echo ${!name}
|
650 | }
|
651 |
|
652 | #
|
653 | # ARITHMETIC ERRORS
|
654 | #
|
655 |
|
656 | unquoted-nounset_arith() {
|
657 | set -o nounset
|
658 | echo $(( x ))
|
659 |
|
660 | echo 'SHOULD NOT GET HERE'
|
661 | }
|
662 |
|
663 | test-divzero() {
|
664 | _osh-error-1 'echo $(( 1 / 0 ))'
|
665 | # Better location
|
666 | _osh-error-1 'echo $(( 1 / (3 -3 ) ))'
|
667 | _osh-error-1 'echo $(( 1 % 0 ))'
|
668 |
|
669 | _osh-error-1 'zero=0; echo $(( 1 / zero ))'
|
670 | _osh-error-1 'zero=0; echo $(( 1 % zero ))'
|
671 |
|
672 | _osh-error-1 '(( a = 1 / 0 )); echo non-fatal; exit 1'
|
673 | _osh-error-1 '(( a = 1 % 0 )); echo non-fatal; exit 1'
|
674 |
|
675 | # fatal!
|
676 | _osh-error-1 'set -e; (( a = 1 / 0 ));'
|
677 | _osh-error-1 'set -e; (( a = 1 % 0 ));'
|
678 | }
|
679 |
|
680 | test-unsafe_arith_eval() {
|
681 | _osh-error-1 '
|
682 | local e1=1+
|
683 | local e2="e1 + 5"
|
684 | echo $(( e2 )) # recursively references e1
|
685 | '
|
686 | }
|
687 |
|
688 | test-unset_expr() {
|
689 | _osh-error-1 'unset -v 1[1]'
|
690 | _osh-error-2 'unset -v 1+2'
|
691 | }
|
692 |
|
693 | test-strict-arith() {
|
694 | _osh-error-1 'shopt -s strict_arith; echo $(( undef[0] ))'
|
695 | _osh-error-1 'shopt -s strict_arith; s=abc; echo $(( s[0] ))'
|
696 | _osh-error-1 'shopt -s strict_arith; var i = 42; echo $(( i[0] ))'
|
697 | }
|
698 |
|
699 | # Only dash flags this as an error.
|
700 | unquoted-string_to_int_arith() {
|
701 | local x='ZZZ'
|
702 | echo $(( x + 5 ))
|
703 |
|
704 | shopt -s strict_arith
|
705 |
|
706 | echo $(( x + 5 ))
|
707 |
|
708 | echo 'SHOULD NOT GET HERE'
|
709 | }
|
710 |
|
711 | # Hm bash treats this as a fatal error
|
712 | unquoted-string_to_hex() {
|
713 | echo $(( 0xGG + 1 ))
|
714 |
|
715 | echo 'SHOULD NOT GET HERE'
|
716 | }
|
717 |
|
718 | # Hm bash treats this as a fatal error
|
719 | unquoted-string_to_octal() {
|
720 | echo $(( 018 + 1 ))
|
721 |
|
722 | echo 'SHOULD NOT GET HERE'
|
723 | }
|
724 |
|
725 | # Hm bash treats this as a fatal error
|
726 | unquoted-string_to_intbase() {
|
727 | echo $(( 16#GG ))
|
728 |
|
729 | echo 'SHOULD NOT GET HERE'
|
730 | }
|
731 |
|
732 | unquoted-undef_arith() {
|
733 | (( undef++ )) # doesn't make sense
|
734 |
|
735 | # Can't assign to characters of string? Is that strong?
|
736 | (( undef[42]++ ))
|
737 | }
|
738 |
|
739 | unquoted-undef_arith2() {
|
740 | a=()
|
741 |
|
742 | # undefined cell: This is kind of what happens in awk / "wok"
|
743 | (( a[42]++ ))
|
744 | (( a[42]++ ))
|
745 | spec/bin/argv.py "${a[@]}"
|
746 | }
|
747 |
|
748 | unquoted-array_arith() {
|
749 | a=(1 2)
|
750 | (( a++ )) # doesn't make sense
|
751 | echo "${a[@]}"
|
752 | }
|
753 |
|
754 | unquoted-undef-assoc-array() {
|
755 | declare -A A
|
756 | A['foo']=bar
|
757 | echo "${A['foo']}"
|
758 |
|
759 | A['spam']+=1
|
760 | A['spam']+=1
|
761 |
|
762 | spec/bin/argv.py "${A[@]}"
|
763 |
|
764 | (( A['spam']+=5 ))
|
765 |
|
766 | spec/bin/argv.py "${A[@]}"
|
767 | }
|
768 |
|
769 | test-assoc-array() {
|
770 | _osh-error-1 'declare -A assoc; assoc[x]=1'
|
771 | _osh-should-run 'declare -A assoc; assoc[$key]=1'
|
772 |
|
773 | # double quotes
|
774 | _osh-should-run 'declare -A assoc; assoc["x"]=1'
|
775 |
|
776 | # single quotes
|
777 | _osh-should-run-here <<'EOF'
|
778 | declare -A assoc; assoc['x']=1
|
779 | EOF
|
780 |
|
781 | _osh-error-1 'declare -A assoc; echo ${assoc[x]}'
|
782 | _osh-should-run 'declare -A assoc; echo ${assoc["x"]}'
|
783 | _osh-should-run 'declare -A assoc; echo ${assoc[$key]}'
|
784 |
|
785 | _osh-error-1 'declare -A assoc; key=k; unset assoc[$key]'
|
786 | # quotes removed
|
787 | _osh-error-1 'declare -A assoc; key=k; unset "assoc[$key]"'
|
788 |
|
789 | # Like Samuel's Nix error
|
790 | # unset -v "hardeningEnableMap[$flag]"
|
791 | _osh-error-here-X 1 <<'EOF'
|
792 | declare -A assoc; key=k; unset "assoc[$key]"
|
793 | EOF
|
794 |
|
795 | # SINGLE quotes fixes it
|
796 | _osh-should-run-here <<'EOF'
|
797 | declare -A assoc; key=k; unset 'assoc[$key]'
|
798 | EOF
|
799 |
|
800 | # Wrap in eval to see how it composes
|
801 | _osh-error-here-X 1 <<'EOF'
|
802 | eval 'declare -A assoc; assoc[x]=1'
|
803 | EOF
|
804 |
|
805 | _osh-error-here-X 1 <<'EOF'
|
806 | eval 'declare -A assoc; unset "assoc[x]"'
|
807 | EOF
|
808 |
|
809 | }
|
810 |
|
811 | unquoted-patsub_bad_glob() {
|
812 | local x='abc'
|
813 | # inspired by git-completion.bash
|
814 | echo ${x//[^]}
|
815 | }
|
816 |
|
817 |
|
818 | #
|
819 | # BOOLEAN ERRORS
|
820 | #
|
821 |
|
822 | # Only osh cares about this.
|
823 | unquoted-string_to_int_bool() {
|
824 | [[ a -eq 0 ]]
|
825 |
|
826 | shopt -s strict_arith
|
827 |
|
828 | [[ a -eq 0 ]]
|
829 | echo 'SHOULD NOT GET HERE'
|
830 | }
|
831 |
|
832 | unquoted-strict_array() {
|
833 | set -- 1 2
|
834 | echo foo > _tmp/"$@"
|
835 | shopt -s strict_array
|
836 | echo foo > _tmp/"$@"
|
837 | }
|
838 |
|
839 | unquoted-strict_array_2() {
|
840 | local foo="$@"
|
841 | shopt -s strict_array
|
842 | local foo="$@"
|
843 | }
|
844 |
|
845 | unquoted-strict_array_3() {
|
846 | local foo=${1:- "[$@]" }
|
847 | shopt -s strict_array
|
848 | local foo=${1:- "[$@]" }
|
849 | }
|
850 |
|
851 | unquoted-strict_array_4() {
|
852 | local -a x
|
853 | x[42]=99
|
854 | echo "x[42] = ${x[42]}"
|
855 |
|
856 | # Not implemented yet
|
857 | shopt -s strict_array
|
858 | local -a y
|
859 | y[42]=99
|
860 | }
|
861 |
|
862 | unquoted-array_assign_1() {
|
863 | s=1
|
864 | s[0]=x # can't assign value
|
865 | }
|
866 |
|
867 | unquoted-array_assign_2() {
|
868 | _osh-error-1 'readonly -a array=(1 2 3); array[0]=x'
|
869 |
|
870 | _osh-error-1 'readonly -a array=(1 2 3); export array'
|
871 | }
|
872 |
|
873 | unquoted-readonly_assign() {
|
874 | _osh-error-1 'readonly x=1; x=2'
|
875 |
|
876 | _osh-error-1 'readonly x=2; y=3 x=99'
|
877 |
|
878 | _osh-error-1 'readonly x=2; declare x=99'
|
879 | _osh-error-1 'readonly x=2; export x=99'
|
880 | }
|
881 |
|
882 | unquoted-multiple_assign() {
|
883 | readonly x=1
|
884 | # It blames x, not a!
|
885 | a=1 b=2 x=42
|
886 | }
|
887 |
|
888 | unquoted-multiple_assign_2() {
|
889 | readonly y
|
890 | local x=1 y=$(( x ))
|
891 | echo $y
|
892 | }
|
893 |
|
894 | unquoted-string_as_array() {
|
895 | local str='foo'
|
896 | echo $str
|
897 | echo "${str[@]}"
|
898 | }
|
899 |
|
900 | #
|
901 | # BUILTINS
|
902 | #
|
903 |
|
904 | unquoted-builtin_bracket() {
|
905 | set +o errexit
|
906 |
|
907 | # xxx is not a valid file descriptor
|
908 | [ -t xxx ]
|
909 | [ -t '' ]
|
910 |
|
911 | [ zz -eq 0 ]
|
912 |
|
913 | # This is from a different evaluator
|
914 | #[ $((a/0)) -eq 0 ]
|
915 | }
|
916 |
|
917 | unquoted-builtin_builtin() {
|
918 | set +o errexit
|
919 | builtin ls
|
920 | }
|
921 |
|
922 | unquoted-builtin_source() {
|
923 | source
|
924 |
|
925 | bad=/nonexistent/path
|
926 | source $bad
|
927 | }
|
928 |
|
929 | unquoted-builtin_cd() {
|
930 | ( unset HOME
|
931 | cd
|
932 | )
|
933 |
|
934 | # TODO: Hm this gives a different useful error without location info
|
935 | ( unset HOME
|
936 | HOME=(a b)
|
937 | cd
|
938 | )
|
939 |
|
940 | # TODO: Hm this gives a different useful error without location info
|
941 | ( unset OLDPWD
|
942 | cd -
|
943 | )
|
944 |
|
945 | ( cd /nonexistent
|
946 | )
|
947 | }
|
948 |
|
949 | unquoted-builtin_pushd() {
|
950 | pushd /nonexistent
|
951 | }
|
952 |
|
953 | unquoted-builtin_popd() {
|
954 | popd # empty dir stack
|
955 |
|
956 | (
|
957 | local dir=$PWD/_tmp/runtime-error-popd
|
958 | mkdir -p $dir
|
959 | pushd $dir
|
960 | pushd /
|
961 | rmdir $dir
|
962 | popd
|
963 | )
|
964 | }
|
965 |
|
966 | unquoted-builtin_unset() {
|
967 | local x=x
|
968 | readonly a
|
969 |
|
970 | unset x a
|
971 | unset -v x a
|
972 | }
|
973 |
|
974 | unquoted-builtin_alias_unalias() {
|
975 | alias zzz
|
976 | unalias zzz
|
977 | }
|
978 |
|
979 | unquoted-builtin_help() {
|
980 | help zzz
|
981 | }
|
982 |
|
983 | unquoted-builtin_trap() {
|
984 | trap
|
985 | trap EXIT
|
986 |
|
987 | trap zzz yyy
|
988 | }
|
989 |
|
990 | unquoted-builtin_getopts() {
|
991 | getopts
|
992 | getopts 'a:'
|
993 |
|
994 | # TODO: It would be nice to put this in a loop and use it properly
|
995 | set -- -a
|
996 | getopts 'a:' varname
|
997 | }
|
998 |
|
999 | builtin_printf() {
|
1000 | printf '%s %d\n' foo not_a_number
|
1001 | echo status=$?
|
1002 |
|
1003 | # bad arg recycling. This is really a runtime error.
|
1004 | printf '%s %d\n' foo 3 bar
|
1005 | echo status=$?
|
1006 |
|
1007 | # invalid width
|
1008 | printf '%*d\n' foo foo
|
1009 | echo status=$?
|
1010 |
|
1011 | # precision can't be specified
|
1012 | printf '%.*d\n' foo foo
|
1013 | echo status=$?
|
1014 |
|
1015 | # precision can't be specified
|
1016 | printf '%.*s\n' foo foo
|
1017 | echo status=$?
|
1018 |
|
1019 | # invalid time
|
1020 | printf '%(%Y)T\n' z
|
1021 | echo status=$?
|
1022 |
|
1023 | # invalid time with no SPID
|
1024 | printf '%(%Y)T\n'
|
1025 | echo status=$?
|
1026 |
|
1027 | # invalid integer with no SPID
|
1028 | printf '%d %d %d\n' 1 2
|
1029 | echo status=$?
|
1030 | }
|
1031 |
|
1032 |
|
1033 | unquoted-builtin_wait() {
|
1034 | wait 1234578
|
1035 | }
|
1036 |
|
1037 | unquoted-builtin_exec() {
|
1038 | exec nonexistent-command 1 2 3
|
1039 | echo $?
|
1040 | }
|
1041 |
|
1042 | #
|
1043 | # Strict options (see spec/strict_options.sh)
|
1044 | #
|
1045 |
|
1046 | unquoted-strict_word_eval_warnings() {
|
1047 | # Warnings when 'set +o strict_word_eval' is OFF
|
1048 |
|
1049 | echo slice start negative
|
1050 | s='abc'
|
1051 | echo -${s: -2}-
|
1052 |
|
1053 | echo slice length negative
|
1054 | s='abc'
|
1055 | echo -${s: 1: -2}-
|
1056 |
|
1057 | # TODO: These need span IDs.
|
1058 | # - invalid utf-8 and also invalid backslash escape
|
1059 |
|
1060 | echo slice bad utf-8
|
1061 | s=$(echo -e "\xFF")bcdef
|
1062 | echo -${s:1:3}-
|
1063 |
|
1064 | echo length bad utf-8
|
1065 | echo ${#s}
|
1066 | }
|
1067 |
|
1068 | unquoted-strict_arith_warnings() {
|
1069 | local x='xx'
|
1070 | echo $(( x + 1 ))
|
1071 |
|
1072 | # TODO: OSH is more lenient here actually
|
1073 | local y='-yy-'
|
1074 | echo $(( y + 1 ))
|
1075 |
|
1076 | [[ $y -eq 0 ]]
|
1077 |
|
1078 | echo 'done'
|
1079 | }
|
1080 |
|
1081 | test-control_flow_subshell() {
|
1082 | _osh-error-1 '
|
1083 | set -o errexit
|
1084 | for i in $(seq 2); do
|
1085 | echo $i
|
1086 | ( break; echo oops)
|
1087 | done
|
1088 | '
|
1089 | }
|
1090 |
|
1091 | test-fallback-locations() {
|
1092 | # Redirect
|
1093 | _osh-error-1 'echo hi > /'
|
1094 |
|
1095 | _osh-error-1 's=x; (( s[0] ))'
|
1096 |
|
1097 | _osh-error-1 's=x; (( s[0] = 42 ))'
|
1098 |
|
1099 | _osh-error-1 'set -u; (( undef ))'
|
1100 |
|
1101 | _osh-error-1 '(( 3 ** -2 ))'
|
1102 | echo
|
1103 |
|
1104 | # DBracket
|
1105 | _osh-error-1 'set -u; [[ $undef =~ . ]]'
|
1106 |
|
1107 | # No good fallback info here, we need it
|
1108 | _osh-error-1 '[[ $x =~ $(( 3 ** -2 )) ]]'
|
1109 |
|
1110 | _osh-error-2 'type -x' # correctly points to -x
|
1111 | _osh-error-2 'use'
|
1112 |
|
1113 | # Assign builtin
|
1114 | _osh-error-2 'export -f'
|
1115 |
|
1116 | _osh-error-1 's=$(true) y=$(( 3 ** -2 ))'
|
1117 |
|
1118 | _osh-error-1 'if s=$(true) y=$(( 3 ** -2 )); then echo hi; fi'
|
1119 |
|
1120 | _osh-error-1 'shopt -s strict_arith; x=a; echo $(( x ))'
|
1121 | _osh-error-1 'shopt -s strict_arith; x=a; echo $(( $x ))'
|
1122 | _osh-error-1 'shopt -s strict_arith; x=a; [[ $x -gt 3 ]]'
|
1123 | _osh-error-1 'shopt -s strict_arith; shopt -u eval_unsafe_arith; x=a; [[ $x -gt 3 ]]'
|
1124 |
|
1125 | _osh-error-1 'shopt -s strict_arith; x=0xgg; echo $(( x ))'
|
1126 |
|
1127 | echo done
|
1128 | }
|
1129 |
|
1130 | test-var-op-qmark() {
|
1131 | _osh-error-1 'echo ${zz?}'
|
1132 | _osh-error-1 'echo ${zz:?}'
|
1133 |
|
1134 | _osh-should-run 'zz=""; echo ${zz?}'
|
1135 | _osh-error-1 'zz=""; echo ${zz:?}'
|
1136 |
|
1137 | _osh-error-1 'echo ${zz?Required}'
|
1138 | _osh-error-1 'echo ${zz:?Required}'
|
1139 | }
|
1140 |
|
1141 | test-external_cmd_typed_args() {
|
1142 | _ysh-error-X 1 'cat ("myfile")'
|
1143 | }
|
1144 |
|
1145 | test-arith-ops-str() {
|
1146 | _ysh-error-X 3 '= "100" + "10a"'
|
1147 | _ysh-error-X 3 '= "100" - "10a"'
|
1148 | _ysh-error-X 3 '= "100" * "10a"'
|
1149 | _ysh-error-X 3 '= "100" / "10a"'
|
1150 | _ysh-error-X 3 'var a = "100"; setvar a += "10a"'
|
1151 | _ysh-error-X 3 'var a = "100"; setvar a -= "10a"'
|
1152 | _ysh-error-X 3 'var a = "100"; setvar a *= "10a"'
|
1153 | _ysh-error-X 3 'var a = "100"; setvar a /= "10a"'
|
1154 | _ysh-error-X 3 '= "age: " + "100"'
|
1155 | _ysh-error-X 3 'var myvar = "a string"
|
1156 | = 100 + myvar'
|
1157 | }
|
1158 |
|
1159 | assert-test-v-error() {
|
1160 | local code=$1
|
1161 |
|
1162 | # note: the test builtin fails with status 2, but the shell doesn't fail
|
1163 | _osh-error-2 "shopt -s strict_word_eval; a=(1 2 3); $code"
|
1164 | }
|
1165 |
|
1166 | test-test-v-expr() {
|
1167 | assert-test-v-error 'test -v ""'
|
1168 | assert-test-v-error 'test -v "a[foo"'
|
1169 | assert-test-v-error 'test -v "a[not-int]"'
|
1170 | assert-test-v-error 'test -v "a[-42]"'
|
1171 |
|
1172 | _osh-error-2 'shopt -s strict_word_eval; s=""; test -v s[0]'
|
1173 | }
|
1174 |
|
1175 | test-long-shell-line() {
|
1176 | # Example from https://github.com/oilshell/oil/issues/1973
|
1177 |
|
1178 | _ysh-error-1 'myvar=$(printf "what a very long string that we have here, which forces the command line to wrap around the terminal width. long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long") && echo $myvar'
|
1179 | echo
|
1180 | }
|
1181 |
|
1182 | test-int-overflow() {
|
1183 | local pos='18446744073709551616'
|
1184 | local neg='-18446744073709551616'
|
1185 |
|
1186 | # frontend/args.py
|
1187 | _osh-error-2 "echo hi | read -n $pos"
|
1188 | _osh-error-2 "echo hi | read -n $neg"
|
1189 |
|
1190 | # osh/sh_expr_eval.py
|
1191 | _osh-error-1 "s=$pos;"' echo $(( $s ))'
|
1192 | _osh-error-1 "s=$neg;"' echo $(( $s ))'
|
1193 |
|
1194 | # octal
|
1195 | local oct_pos='01234567012345670123456701234567'
|
1196 | local oct_neg="-$oct_pos"
|
1197 | _osh-error-1 "s=$oct_pos;"' echo $(( $s ))'
|
1198 | _osh-error-1 "s=$oct_neg;"' echo $(( $s ))' # treated as negation
|
1199 |
|
1200 | # hex
|
1201 | local hex_pos='0x123456789abcdef0123456789'
|
1202 | local hex_neg="-$hex_pos"
|
1203 | _osh-error-1 "s=$hex_pos;"' echo $(( $s ))'
|
1204 | _osh-error-1 "s=$hex_neg;"' echo $(( $s ))' # treated as negation
|
1205 |
|
1206 | # builtins
|
1207 | _osh-error-1 'printf %d'" $pos"
|
1208 | _osh-error-1 'printf %d'" $neg"
|
1209 |
|
1210 | _osh-error-2 "trap $pos ERR"
|
1211 | _osh-error-2 "trap -- $neg ERR"
|
1212 |
|
1213 | _osh-error-2 "ulimit $pos"
|
1214 | _osh-error-2 "ulimit -- $neg"
|
1215 | }
|
1216 |
|
1217 | test-bind() {
|
1218 | _osh-error-2 'bind -x'
|
1219 | _osh-error-1 'bind -x foo'
|
1220 |
|
1221 | _osh-error-1 'bind "a" "b"'
|
1222 | _osh-error-1 'bind "\eQ: yank-pop" "b"'
|
1223 | _osh-error-1 'bind "\eQ: yank-pop" "\eQ: yank-pop" "c"'
|
1224 | }
|
1225 |
|
1226 | #
|
1227 | # TEST DRIVER
|
1228 | #
|
1229 |
|
1230 | list-unquoted-funcs() {
|
1231 | ### These tests need assertions, with quoting
|
1232 |
|
1233 | compgen -A function | egrep '^unquoted-'
|
1234 | }
|
1235 |
|
1236 | run-unquoted-funcs() {
|
1237 | local i=0
|
1238 | list-unquoted-funcs | ( while read -r test_func; do
|
1239 | _run-test-func $test_func '' # don't assert status
|
1240 | i=$((i + 1))
|
1241 | done
|
1242 |
|
1243 | # Hacky subshell for $i
|
1244 | echo
|
1245 | echo "$0: $i unquoted functions run. TODO: migrate to test-* to assert status"
|
1246 | )
|
1247 | }
|
1248 |
|
1249 | all-tests() {
|
1250 |
|
1251 | section-banner 'Runtime errors - Unquoted test functions'
|
1252 | # Legacy functions that don't check status
|
1253 | run-unquoted-funcs
|
1254 |
|
1255 | section-banner 'Runtime errors - test functions'
|
1256 |
|
1257 | # Run with strict mode
|
1258 | set -o nounset
|
1259 | set -o pipefail
|
1260 | set -o errexit
|
1261 |
|
1262 | run-test-funcs
|
1263 | }
|
1264 |
|
1265 | # TODO: could show these as separate text files in the CI
|
1266 |
|
1267 | with-bash() {
|
1268 | SH_ASSERT_DISABLE=1 OSH=bash YSH=bash run-test-funcs
|
1269 | }
|
1270 |
|
1271 | with-dash() {
|
1272 | SH_ASSERT_DISABLE=1 OSH=dash YSH=dash run-test-funcs
|
1273 | }
|
1274 |
|
1275 | soil-run-py() {
|
1276 | all-tests
|
1277 | }
|
1278 |
|
1279 | soil-run-cpp() {
|
1280 | # TODO: There are some UBSAN errors, like downcasting mylib::LineReader.
|
1281 | # Is that a real problem? Could be due to mylib::File.
|
1282 |
|
1283 | #local osh=_bin/cxx-ubsan/osh
|
1284 |
|
1285 | local osh=_bin/cxx-asan/osh
|
1286 |
|
1287 | ninja $osh
|
1288 | OSH=$osh all-tests
|
1289 | }
|
1290 |
|
1291 | run-for-release() {
|
1292 | run-other-suite-for-release runtime-errors all-tests
|
1293 | }
|
1294 |
|
1295 | "$@"
|