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