1
2 ## compare_shells: bash dash mksh ash
3
4 # OSH mechanisms:
5 #
6 # - shopt -s strict_errexit
7 # - shopt -s command_sub_errexit
8 # - inherit_errexit (bash)
9 #
10 # Summary:
11 # - local assignment is different than global! The exit code and errexit
12 # behavior are different because the concept of the "last command" is
13 # different.
14 # - ash has copied bash behavior!
15
16 #### command sub: errexit is NOT inherited and outer shell keeps going
17
18 # This is the bash-specific bug here:
19 # https://blogs.janestreet.com/when-bash-scripts-bite/
20 # See inherit_errexit below.
21 #
22 # I remember finding a script that relies on bash's bad behavior, so OSH copies
23 # it. But you can opt in to better behavior.
24
25 set -o errexit
26 echo $(echo one; false; echo two) # bash/ash keep going
27 echo parent status=$?
28 ## STDOUT:
29 one two
30 parent status=0
31 ## END
32 # dash and mksh: inner shell aborts, but outer one keeps going!
33 ## OK dash/mksh STDOUT:
34 one
35 parent status=0
36 ## END
37
38 #### command sub with inherit_errexit only
39 set -o errexit
40 shopt -s inherit_errexit || true
41 echo zero
42 echo $(echo one; false; echo two) # bash/ash keep going
43 echo parent status=$?
44 ## STDOUT:
45 zero
46 one
47 parent status=0
48 ## END
49 ## N-I ash STDOUT:
50 zero
51 one two
52 parent status=0
53 ## END
54
55 #### strict_errexit and assignment builtins (local, export, readonly ...)
56 set -o errexit
57 shopt -s strict_errexit || true
58 #shopt -s command_sub_errexit || true
59
60 f() {
61 local x=$(echo hi; false)
62 echo x=$x
63 }
64
65 eval 'f'
66 echo ---
67
68 ## status: 1
69 ## STDOUT:
70 ## END
71 ## N-I dash/bash/mksh/ash status: 0
72 ## N-I dash/bash/mksh/ash STDOUT:
73 x=hi
74 ---
75 ## END
76
77 #### strict_errexit and command sub in export / readonly
78 case $SH in (dash|bash|mksh|ash) exit ;; esac
79
80 $SH -o errexit -O strict_errexit -c 'echo a; export x=$(might-fail); echo b'
81 echo status=$?
82 $SH -o errexit -O strict_errexit -c 'echo a; readonly x=$(might-fail); echo b'
83 echo status=$?
84 $SH -o errexit -O strict_errexit -c 'echo a; x=$(true); echo b'
85 echo status=$?
86
87 ## STDOUT:
88 a
89 status=1
90 a
91 status=1
92 a
93 b
94 status=0
95 ## END
96 ## N-I dash/bash/mksh/ash stdout-json: ""
97
98
99 #### strict_errexit disallows pipeline
100 set -o errexit
101 shopt -s strict_errexit || true
102
103 if echo 1 | grep 1; then
104 echo one
105 fi
106
107 ## status: 1
108 ## N-I dash/bash/mksh/ash status: 0
109 ## N-I dash/bash/mksh/ash STDOUT:
110 1
111 one
112 ## END
113
114 #### strict_errexit allows singleton pipeline
115 set -o errexit
116 shopt -s strict_errexit || true
117
118 if ! false; then
119 echo yes
120 fi
121
122 ## STDOUT:
123 yes
124 ## END
125
126 #### strict_errexit with && || !
127 set -o errexit
128 shopt -s strict_errexit || true
129
130 if true && true; then
131 echo A
132 fi
133
134 if true || false; then
135 echo B
136 fi
137
138 if ! false && ! false; then
139 echo C
140 fi
141
142 ## STDOUT:
143 A
144 B
145 C
146 ## END
147
148 #### strict_errexit detects proc in && || !
149 set -o errexit
150 shopt -s strict_errexit || true
151
152 myfunc() {
153 echo 'failing'
154 false
155 echo 'should not get here'
156 }
157
158 if true && ! myfunc; then
159 echo B
160 fi
161
162 if ! myfunc; then
163 echo A
164 fi
165
166 ## status: 1
167 ## STDOUT:
168 ## END
169
170 # POSIX shell behavior:
171
172 ## OK bash/dash/mksh/ash status: 0
173 ## OK bash/dash/mksh/ash STDOUT:
174 failing
175 should not get here
176 failing
177 should not get here
178 ## END
179
180
181
182 #### strict_errexit without errexit proc
183 myproc() {
184 echo myproc
185 }
186 myproc || true
187
188 # This should be a no-op I guess
189 shopt -s strict_errexit || true
190 myproc || true
191
192 ## status: 1
193 ## STDOUT:
194 myproc
195 ## END
196 ## N-I dash/bash/mksh/ash status: 0
197 ## N-I dash/bash/mksh/ash STDOUT:
198 myproc
199 myproc
200 ## END
201
202 #### strict_errexit without errexit proc / command sub
203
204 # Implementation quirk:
205 # - The proc check happens only if errexit WAS on and is disabled
206 # - But 'shopt --unset allow_csub_psub' happens if it was never on
207
208 shopt -s strict_errexit || true
209
210 p() {
211 echo before
212 local x
213 # This line fails, which is a bit weird, but errexit
214 x=$(false)
215 echo x=$x
216 }
217
218 if p; then
219 echo ok
220 fi
221
222 ## N-I dash/bash/mksh/ash status: 0
223 ## N-I dash/bash/mksh/ash STDOUT:
224 before
225 x=
226 ok
227 ## END
228 ## status: 1
229 ## STDOUT:
230 ## END
231
232 #### strict_errexit and errexit disabled
233 case $SH in (dash|bash|mksh|ash) exit ;; esac
234
235 shopt -s parse_brace strict_errexit || true
236
237 p() {
238 echo before
239 local x
240 # This line fails, which is a bit weird, but errexit
241 x=$(false)
242 echo x=$x
243 }
244
245 set -o errexit
246 shopt --unset errexit {
247 # It runs normally here, because errexit was disabled (just not by a
248 # conditional)
249 p
250 }
251 ## N-I dash/bash/mksh/ash STDOUT:
252 ## END
253 ## STDOUT:
254 before
255 x=
256 ## END
257
258
259 #### command sub with command_sub_errexit only
260 set -o errexit
261 shopt -s command_sub_errexit || true
262 echo zero
263 echo $(echo one; false; echo two) # bash/ash keep going
264 echo parent status=$?
265 ## STDOUT:
266 zero
267 one two
268 parent status=0
269 ## END
270 ## N-I dash/mksh STDOUT:
271 zero
272 one
273 parent status=0
274 ## END
275
276 #### command_sub_errexit stops at first error
277 case $SH in (dash|bash|mksh|ash) exit ;; esac
278
279 set -o errexit
280 shopt --set parse_brace command_sub_errexit verbose_errexit || true
281
282 rm -f BAD
283
284 try {
285 echo $(date %d) $(touch BAD)
286 }
287 if ! test -f BAD; then # should not exist
288 echo OK
289 fi
290
291 ## STDOUT:
292 OK
293 ## END
294 ## N-I dash/bash/mksh/ash STDOUT:
295 ## END
296
297 #### command sub with inherit_errexit and command_sub_errexit
298 set -o errexit
299
300 # bash implements inherit_errexit, but it's not as strict as OSH.
301 shopt -s inherit_errexit || true
302 shopt -s command_sub_errexit || true
303 echo zero
304 echo $(echo one; false; echo two) # bash/ash keep going
305 echo parent status=$?
306 ## STDOUT:
307 zero
308 ## END
309 ## status: 1
310 ## N-I dash/mksh/bash status: 0
311 ## N-I dash/mksh/bash STDOUT:
312 zero
313 one
314 parent status=0
315 ## END
316 ## N-I ash status: 0
317 ## N-I ash STDOUT:
318 zero
319 one two
320 parent status=0
321 ## END
322
323 #### command sub: last command fails but keeps going and exit code is 0
324 set -o errexit
325 echo $(echo one; false) # we lost the exit code
326 echo status=$?
327 ## STDOUT:
328 one
329 status=0
330 ## END
331
332 #### global assignment with command sub: middle command fails
333 set -o errexit
334 s=$(echo one; false; echo two;)
335 echo "$s"
336 ## status: 0
337 ## STDOUT:
338 one
339 two
340 ## END
341 # dash and mksh: whole thing aborts!
342 ## OK dash/mksh stdout-json: ""
343 ## OK dash/mksh status: 1
344
345 #### global assignment with command sub: last command fails and it aborts
346 set -o errexit
347 s=$(echo one; false)
348 echo status=$?
349 ## stdout-json: ""
350 ## status: 1
351
352 #### local: middle command fails and keeps going
353 set -o errexit
354 f() {
355 echo good
356 local x=$(echo one; false; echo two)
357 echo status=$?
358 echo $x
359 }
360 f
361 ## STDOUT:
362 good
363 status=0
364 one two
365 ## END
366 # for dash and mksh, the INNER shell aborts, but the outer one keeps going!
367 ## OK dash/mksh STDOUT:
368 good
369 status=0
370 one
371 ## END
372
373 #### local: last command fails and also keeps going
374 set -o errexit
375 f() {
376 echo good
377 local x=$(echo one; false)
378 echo status=$?
379 echo $x
380 }
381 f
382 ## STDOUT:
383 good
384 status=0
385 one
386 ## END
387
388 #### local and inherit_errexit / command_sub_errexit
389 # I've run into this problem a lot.
390 set -o errexit
391 shopt -s inherit_errexit || true # bash option
392 shopt -s command_sub_errexit || true # oil option
393 f() {
394 echo good
395 local x=$(echo one; false; echo two)
396 echo status=$?
397 echo $x
398 }
399 f
400 ## status: 1
401 ## STDOUT:
402 good
403 ## END
404 ## N-I ash status: 0
405 ## N-I ash STDOUT:
406 good
407 status=0
408 one two
409 ## END
410 ## N-I bash/dash/mksh status: 0
411 ## N-I bash/dash/mksh STDOUT:
412 good
413 status=0
414 one
415 ## END
416
417 #### global assignment when last status is failure
418 # this is a bug I introduced
419 set -o errexit
420 x=$(false) || true # from abuild
421 [ -n "$APORTSDIR" ] && true
422 BUILDDIR=${_BUILDDIR-$BUILDDIR}
423 echo status=$?
424 ## STDOUT:
425 status=0
426 ## END
427
428 #### strict_errexit prevents errexit from being disabled in function
429 set -o errexit
430 fun() { echo fun; }
431
432 fun || true # this is OK
433
434 shopt -s strict_errexit || true
435
436 echo 'builtin ok' || true
437 env echo 'external ok' || true
438
439 fun || true # this fails
440
441 ## status: 1
442 ## STDOUT:
443 fun
444 builtin ok
445 external ok
446 ## END
447 ## N-I dash/bash/mksh/ash status: 0
448 ## N-I dash/bash/mksh/ash STDOUT:
449 fun
450 builtin ok
451 external ok
452 fun
453 ## END
454
455 #### strict_errexit prevents errexit from being disabled in brace group
456 set -o errexit
457 # false failure is NOT respected either way
458 { echo foo; false; echo bar; } || echo "failed"
459
460 shopt -s strict_errexit || true
461 { echo foo; false; echo bar; } || echo "failed"
462 ## status: 1
463 ## STDOUT:
464 foo
465 bar
466 ## END
467
468 ## N-I dash/bash/mksh/ash status: 0
469 ## N-I dash/bash/mksh/ash STDOUT:
470 foo
471 bar
472 foo
473 bar
474 ## END
475
476 #### strict_errexit prevents errexit from being disabled in subshell
477 set -o errexit
478 shopt -s inherit_errexit || true
479
480 # false failure is NOT respected either way
481 ( echo foo; false; echo bar; ) || echo "failed"
482
483 shopt -s strict_errexit || true
484 ( echo foo; false; echo bar; ) || echo "failed"
485 ## status: 1
486 ## STDOUT:
487 foo
488 bar
489 ## END
490
491 ## N-I dash/bash/mksh/ash status: 0
492 ## N-I dash/bash/mksh/ash STDOUT:
493 foo
494 bar
495 foo
496 bar
497 ## END
498
499 #### strict_errexit and ! && || if while until
500 prelude='set -o errexit
501 shopt -s strict_errexit || true
502 fun() { echo fun; }'
503
504 $SH -c "$prelude; ! fun; echo 'should not get here'"
505 echo bang=$?
506 echo --
507
508 $SH -c "$prelude; fun || true"
509 echo or=$?
510 echo --
511
512 $SH -c "$prelude; fun && true"
513 echo and=$?
514 echo --
515
516 $SH -c "$prelude; if fun; then true; fi"
517 echo if=$?
518 echo --
519
520 $SH -c "$prelude; while fun; do echo while; exit; done"
521 echo while=$?
522 echo --
523
524 $SH -c "$prelude; until fun; do echo until; exit; done"
525 echo until=$?
526 echo --
527
528
529 ## STDOUT:
530 bang=1
531 --
532 or=1
533 --
534 and=1
535 --
536 if=1
537 --
538 while=1
539 --
540 until=1
541 --
542 ## END
543 ## N-I dash/bash/mksh/ash STDOUT:
544 fun
545 should not get here
546 bang=0
547 --
548 fun
549 or=0
550 --
551 fun
552 and=0
553 --
554 fun
555 if=0
556 --
557 fun
558 while
559 while=0
560 --
561 fun
562 until=0
563 --
564 ## END
565
566 #### if pipeline doesn't fail fatally
567 set -o errexit
568 set -o pipefail
569
570 f() {
571 local dir=$1
572 if ls $dir | grep ''; then
573 echo foo
574 echo ${PIPESTATUS[@]}
575 fi
576 }
577 rmdir $TMP/_tmp || true
578 rm -f $TMP/*
579 f $TMP
580 f /nonexistent # should fail
581 echo done
582
583 ## N-I dash status: 2
584 ## N-I dash stdout-json: ""
585 ## STDOUT:
586 done
587 ## END
588
589 #### errexit is silent (verbose_errexit for Oil)
590 shopt -u verbose_errexit 2>/dev/null || true
591 set -e
592 false
593 ## stderr-json: ""
594 ## status: 1
595
596 #### command sub errexit preserves exit code
597 set -e
598 shopt -s command_sub_errexit || true
599
600 echo before
601 echo $(exit 42)
602 echo after
603 ## STDOUT:
604 before
605 ## END
606 ## status: 42
607 ## N-I dash/bash/mksh/ash STDOUT:
608 before
609
610 after
611 ## N-I dash/bash/mksh/ash status: 0
612
613 #### What's in strict:all?
614
615 # inherit_errexit, strict_errexit, but not command_sub_errexit!
616 # for that you need oil:upgrade!
617
618 set -o errexit
619 shopt -s strict:all || true
620
621 # inherit_errexit is bash compatible, so we have it
622 #echo $(date %x)
623
624 # command_sub_errexit would hide errors!
625 f() {
626 local d=$(date %x)
627 }
628 f
629
630 deploy_func() {
631 echo one
632 false
633 echo two
634 }
635
636 if ! deploy_func; then
637 echo failed
638 fi
639
640 echo 'should not get here'
641
642 ## status: 1
643 ## STDOUT:
644 ## END
645 ## N-I dash/bash/mksh/ash status: 0
646 ## N-I dash/bash/mksh/ash STDOUT:
647 one
648 two
649 should not get here
650 ## END
651
652 #### command_sub_errexit causes local d=$(date %x) to fail
653 set -o errexit
654 shopt -s inherit_errexit || true
655 #shopt -s strict_errexit || true
656 shopt -s command_sub_errexit || true
657
658 myproc() {
659 # this is disallowed because we want a runtime error 100% of the time
660 local x=$(true)
661
662 # Realistic example. Should fail here but shells don't!
663 local d=$(date %x)
664 echo hi
665 }
666 myproc
667
668 ## status: 1
669 ## STDOUT:
670 ## END
671 ## N-I dash/bash/mksh/ash status: 0
672 ## N-I dash/bash/mksh/ash STDOUT:
673 hi
674 ## END
675
676 #### command_sub_errexit and command sub in array
677 case $SH in (dash|ash|mksh) exit ;; esac
678
679 set -o errexit
680 shopt -s inherit_errexit || true
681 #shopt -s strict_errexit || true
682 shopt -s command_sub_errexit || true
683
684 # We don't want silent failure here
685 readonly -a myarray=( one "$(date %x)" two )
686
687 #echo len=${#myarray[@]}
688 argv.py "${myarray[@]}"
689 ## status: 1
690 ## STDOUT:
691 ## END
692 ## N-I bash status: 0
693 ## N-I bash STDOUT:
694 ['one', '', 'two']
695 ## END
696 ## N-I dash/ash/mksh status: 0
697
698 #### OLD: command sub in conditional, with inherit_errexit
699 set -o errexit
700 shopt -s inherit_errexit || true
701 if echo $(echo 1; false; echo 2); then
702 echo A
703 fi
704 echo done
705
706 ## STDOUT:
707 1 2
708 A
709 done
710 ## END
711 ## N-I dash/mksh STDOUT:
712 1
713 A
714 done
715 ## END
716
717 #### OLD: command sub in redirect in conditional
718 set -o errexit
719
720 if echo tmp_contents > $(echo tmp); then
721 echo 2
722 fi
723 cat tmp
724 ## STDOUT:
725 2
726 tmp_contents
727 ## END
728
729 #### Regression
730 case $SH in (bash|dash|ash|mksh) exit ;; esac
731
732 shopt --set oil:upgrade
733
734 shopt --unset errexit {
735 echo hi
736 }
737
738 proc p {
739 echo p
740 }
741
742 shopt --unset errexit {
743 p
744 }
745 ## STDOUT:
746 hi
747 p
748 ## END
749 ## N-I bash/dash/ash/mksh stdout-json: ""
750
751 #### ShAssignment used as conditional
752
753 while x=$(false)
754 do
755 echo while
756 done
757
758 if x=$(false)
759 then
760 echo if
761 fi
762
763 if x=$(true)
764 then
765 echo yes
766 fi
767
768 # Same thing with errexit -- NOT affected
769 set -o errexit
770
771 while x=$(false)
772 do
773 echo while
774 done
775
776 if x=$(false)
777 then
778 echo if
779 fi
780
781 if x=$(true)
782 then
783 echo yes
784 fi
785
786 # Same thing with strict_errexit -- NOT affected
787 shopt -s strict_errexit || true
788
789 while x=$(false)
790 do
791 echo while
792 done
793
794 if x=$(false)
795 then
796 echo if
797 fi
798
799 if x=$(true)
800 then
801 echo yes
802 fi
803
804 ## status: 1
805 ## STDOUT:
806 yes
807 yes
808 ## END
809 ## N-I dash/bash/mksh/ash status: 0
810 ## N-I dash/bash/mksh/ash STDOUT:
811 yes
812 yes
813 yes
814 ## END