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 without errexit proc
127 myproc() {
128 echo myproc
129 }
130 myproc || true
131
132 # This should be a no-op I guess
133 shopt -s strict_errexit || true
134 myproc || true
135
136 ## status: 1
137 ## STDOUT:
138 myproc
139 ## END
140 ## N-I dash/bash/mksh/ash status: 0
141 ## N-I dash/bash/mksh/ash STDOUT:
142 myproc
143 myproc
144 ## END
145
146 #### strict_errexit without errexit proc / command sub
147
148 # Implementation quirk:
149 # - The proc check happens only if errexit WAS on and is disabled
150 # - But 'shopt --unset allow_csub_psub' happens if it was never on
151
152 shopt -s strict_errexit || true
153
154 p() {
155 echo before
156 local x
157 # This line fails, which is a bit weird, but errexit
158 x=$(false)
159 echo x=$x
160 }
161
162 if p; then
163 echo ok
164 fi
165
166 ## N-I dash/bash/mksh/ash status: 0
167 ## N-I dash/bash/mksh/ash STDOUT:
168 before
169 x=
170 ok
171 ## END
172 ## status: 1
173 ## STDOUT:
174 ## END
175
176 #### strict_errexit and errexit disabled
177 case $SH in (dash|bash|mksh|ash) exit ;; esac
178
179 shopt -s parse_brace strict_errexit || true
180
181 p() {
182 echo before
183 local x
184 # This line fails, which is a bit weird, but errexit
185 x=$(false)
186 echo x=$x
187 }
188
189 set -o errexit
190 shopt --unset errexit {
191 # It runs normally here, because errexit was disabled (just not by a
192 # conditional)
193 p
194 }
195 ## N-I dash/bash/mksh/ash STDOUT:
196 ## END
197 ## STDOUT:
198 before
199 x=
200 ## END
201
202
203 #### command sub with command_sub_errexit only
204 set -o errexit
205 shopt -s command_sub_errexit || true
206 echo zero
207 echo $(echo one; false; echo two) # bash/ash keep going
208 echo parent status=$?
209 ## STDOUT:
210 zero
211 one two
212 parent status=0
213 ## END
214 ## N-I dash/mksh STDOUT:
215 zero
216 one
217 parent status=0
218 ## END
219
220 #### command_sub_errexit stops at first error
221 case $SH in (dash|bash|mksh|ash) exit ;; esac
222
223 set -o errexit
224 shopt --set parse_brace command_sub_errexit verbose_errexit || true
225
226 rm -f BAD
227
228 try {
229 echo $(date %d) $(touch BAD)
230 }
231 if ! test -f BAD; then # should not exist
232 echo OK
233 fi
234
235 ## STDOUT:
236 OK
237 ## END
238 ## N-I dash/bash/mksh/ash STDOUT:
239 ## END
240
241 #### command sub with inherit_errexit and command_sub_errexit
242 set -o errexit
243
244 # bash implements inherit_errexit, but it's not as strict as OSH.
245 shopt -s inherit_errexit || true
246 shopt -s command_sub_errexit || true
247 echo zero
248 echo $(echo one; false; echo two) # bash/ash keep going
249 echo parent status=$?
250 ## STDOUT:
251 zero
252 ## END
253 ## status: 1
254 ## N-I dash/mksh/bash status: 0
255 ## N-I dash/mksh/bash STDOUT:
256 zero
257 one
258 parent status=0
259 ## END
260 ## N-I ash status: 0
261 ## N-I ash STDOUT:
262 zero
263 one two
264 parent status=0
265 ## END
266
267 #### command sub: last command fails but keeps going and exit code is 0
268 set -o errexit
269 echo $(echo one; false) # we lost the exit code
270 echo status=$?
271 ## STDOUT:
272 one
273 status=0
274 ## END
275
276 #### global assignment with command sub: middle command fails
277 set -o errexit
278 s=$(echo one; false; echo two;)
279 echo "$s"
280 ## status: 0
281 ## STDOUT:
282 one
283 two
284 ## END
285 # dash and mksh: whole thing aborts!
286 ## OK dash/mksh stdout-json: ""
287 ## OK dash/mksh status: 1
288
289 #### global assignment with command sub: last command fails and it aborts
290 set -o errexit
291 s=$(echo one; false)
292 echo status=$?
293 ## stdout-json: ""
294 ## status: 1
295
296 #### local: middle command fails and keeps going
297 set -o errexit
298 f() {
299 echo good
300 local x=$(echo one; false; echo two)
301 echo status=$?
302 echo $x
303 }
304 f
305 ## STDOUT:
306 good
307 status=0
308 one two
309 ## END
310 # for dash and mksh, the INNER shell aborts, but the outer one keeps going!
311 ## OK dash/mksh STDOUT:
312 good
313 status=0
314 one
315 ## END
316
317 #### local: last command fails and also keeps going
318 set -o errexit
319 f() {
320 echo good
321 local x=$(echo one; false)
322 echo status=$?
323 echo $x
324 }
325 f
326 ## STDOUT:
327 good
328 status=0
329 one
330 ## END
331
332 #### local and inherit_errexit / command_sub_errexit
333 # I've run into this problem a lot.
334 set -o errexit
335 shopt -s inherit_errexit || true # bash option
336 shopt -s command_sub_errexit || true # oil option
337 f() {
338 echo good
339 local x=$(echo one; false; echo two)
340 echo status=$?
341 echo $x
342 }
343 f
344 ## status: 1
345 ## STDOUT:
346 good
347 ## END
348 ## N-I ash status: 0
349 ## N-I ash STDOUT:
350 good
351 status=0
352 one two
353 ## END
354 ## N-I bash/dash/mksh status: 0
355 ## N-I bash/dash/mksh STDOUT:
356 good
357 status=0
358 one
359 ## END
360
361 #### global assignment when last status is failure
362 # this is a bug I introduced
363 set -o errexit
364 x=$(false) || true # from abuild
365 [ -n "$APORTSDIR" ] && true
366 BUILDDIR=${_BUILDDIR-$BUILDDIR}
367 echo status=$?
368 ## STDOUT:
369 status=0
370 ## END
371
372 #### strict_errexit prevents errexit from being disabled in function
373 set -o errexit
374 fun() { echo fun; }
375
376 fun || true # this is OK
377
378 shopt -s strict_errexit || true
379
380 echo 'builtin ok' || true
381 env echo 'external ok' || true
382
383 fun || true # this fails
384
385 ## status: 1
386 ## STDOUT:
387 fun
388 builtin ok
389 external ok
390 ## END
391 ## N-I dash/bash/mksh/ash status: 0
392 ## N-I dash/bash/mksh/ash STDOUT:
393 fun
394 builtin ok
395 external ok
396 fun
397 ## END
398
399 #### strict_errexit prevents errexit from being disabled in brace group
400 set -o errexit
401 # false failure is NOT respected either way
402 { echo foo; false; echo bar; } || echo "failed"
403
404 shopt -s strict_errexit || true
405 { echo foo; false; echo bar; } || echo "failed"
406 ## status: 1
407 ## STDOUT:
408 foo
409 bar
410 ## END
411
412 ## N-I dash/bash/mksh/ash status: 0
413 ## N-I dash/bash/mksh/ash STDOUT:
414 foo
415 bar
416 foo
417 bar
418 ## END
419
420 #### strict_errexit prevents errexit from being disabled in subshell
421 set -o errexit
422 shopt -s inherit_errexit || true
423
424 # false failure is NOT respected either way
425 ( echo foo; false; echo bar; ) || echo "failed"
426
427 shopt -s strict_errexit || true
428 ( echo foo; false; echo bar; ) || echo "failed"
429 ## status: 1
430 ## STDOUT:
431 foo
432 bar
433 ## END
434
435 ## N-I dash/bash/mksh/ash status: 0
436 ## N-I dash/bash/mksh/ash STDOUT:
437 foo
438 bar
439 foo
440 bar
441 ## END
442
443 #### strict_errexit and ! && || if while until
444 prelude='set -o errexit
445 shopt -s strict_errexit || true
446 fun() { echo fun; }'
447
448 $SH -c "$prelude; ! fun; echo 'should not get here'"
449 echo bang=$?
450 echo --
451
452 $SH -c "$prelude; fun || true"
453 echo or=$?
454 echo --
455
456 $SH -c "$prelude; fun && true"
457 echo and=$?
458 echo --
459
460 $SH -c "$prelude; if fun; then true; fi"
461 echo if=$?
462 echo --
463
464 $SH -c "$prelude; while fun; do echo while; exit; done"
465 echo while=$?
466 echo --
467
468 $SH -c "$prelude; until fun; do echo until; exit; done"
469 echo until=$?
470 echo --
471
472
473 ## STDOUT:
474 bang=1
475 --
476 or=1
477 --
478 and=1
479 --
480 if=1
481 --
482 while=1
483 --
484 until=1
485 --
486 ## END
487 ## N-I dash/bash/mksh/ash STDOUT:
488 fun
489 should not get here
490 bang=0
491 --
492 fun
493 or=0
494 --
495 fun
496 and=0
497 --
498 fun
499 if=0
500 --
501 fun
502 while
503 while=0
504 --
505 fun
506 until=0
507 --
508 ## END
509
510 #### if pipeline doesn't fail fatally
511 set -o errexit
512 set -o pipefail
513
514 f() {
515 local dir=$1
516 if ls $dir | grep ''; then
517 echo foo
518 echo ${PIPESTATUS[@]}
519 fi
520 }
521 rmdir $TMP/_tmp || true
522 rm -f $TMP/*
523 f $TMP
524 f /nonexistent # should fail
525 echo done
526
527 ## N-I dash status: 2
528 ## N-I dash stdout-json: ""
529 ## STDOUT:
530 done
531 ## END
532
533 #### errexit is silent (verbose_errexit for Oil)
534 shopt -u verbose_errexit 2>/dev/null || true
535 set -e
536 false
537 ## stderr-json: ""
538 ## status: 1
539
540 #### command sub errexit preserves exit code
541 set -e
542 shopt -s command_sub_errexit || true
543
544 echo before
545 echo $(exit 42)
546 echo after
547 ## STDOUT:
548 before
549 ## END
550 ## status: 42
551 ## N-I dash/bash/mksh/ash STDOUT:
552 before
553
554 after
555 ## N-I dash/bash/mksh/ash status: 0
556
557 #### What's in strict:all?
558
559 # inherit_errexit, strict_errexit, but not command_sub_errexit!
560 # for that you need oil:upgrade!
561
562 set -o errexit
563 shopt -s strict:all || true
564
565 # inherit_errexit is bash compatible, so we have it
566 #echo $(date %x)
567
568 # command_sub_errexit would hide errors!
569 f() {
570 local d=$(date %x)
571 }
572 f
573
574 deploy_func() {
575 echo one
576 false
577 echo two
578 }
579
580 if ! deploy_func; then
581 echo failed
582 fi
583
584 echo 'should not get here'
585
586 ## status: 1
587 ## STDOUT:
588 ## END
589 ## N-I dash/bash/mksh/ash status: 0
590 ## N-I dash/bash/mksh/ash STDOUT:
591 one
592 two
593 should not get here
594 ## END
595
596 #### command_sub_errexit causes local d=$(date %x) to fail
597 set -o errexit
598 shopt -s inherit_errexit || true
599 #shopt -s strict_errexit || true
600 shopt -s command_sub_errexit || true
601
602 myproc() {
603 # this is disallowed because we want a runtime error 100% of the time
604 local x=$(true)
605
606 # Realistic example. Should fail here but shells don't!
607 local d=$(date %x)
608 echo hi
609 }
610 myproc
611
612 ## status: 1
613 ## STDOUT:
614 ## END
615 ## N-I dash/bash/mksh/ash status: 0
616 ## N-I dash/bash/mksh/ash STDOUT:
617 hi
618 ## END
619
620 #### command_sub_errexit and command sub in array
621 case $SH in (dash|ash|mksh) exit ;; esac
622
623 set -o errexit
624 shopt -s inherit_errexit || true
625 #shopt -s strict_errexit || true
626 shopt -s command_sub_errexit || true
627
628 # We don't want silent failure here
629 readonly -a myarray=( one "$(date %x)" two )
630
631 #echo len=${#myarray[@]}
632 argv.py "${myarray[@]}"
633 ## status: 1
634 ## STDOUT:
635 ## END
636 ## N-I bash status: 0
637 ## N-I bash STDOUT:
638 ['one', '', 'two']
639 ## END
640 ## N-I dash/ash/mksh status: 0
641
642 #### OLD: command sub in conditional, with inherit_errexit
643 set -o errexit
644 shopt -s inherit_errexit || true
645 if echo $(echo 1; false; echo 2); then
646 echo A
647 fi
648 echo done
649
650 ## STDOUT:
651 1 2
652 A
653 done
654 ## END
655 ## N-I dash/mksh STDOUT:
656 1
657 A
658 done
659 ## END
660
661 #### OLD: command sub in redirect in conditional
662 set -o errexit
663
664 if echo tmp_contents > $(echo tmp); then
665 echo 2
666 fi
667 cat tmp
668 ## STDOUT:
669 2
670 tmp_contents
671 ## END
672
673 #### Regression
674 case $SH in (bash|dash|ash|mksh) exit ;; esac
675
676 shopt --set oil:upgrade
677
678 shopt --unset errexit {
679 echo hi
680 }
681
682 proc p {
683 echo p
684 }
685
686 shopt --unset errexit {
687 p
688 }
689 ## STDOUT:
690 hi
691 p
692 ## END
693 ## N-I bash/dash/ash/mksh stdout-json: ""
694
695 #### ShAssignment used as conditional
696
697 while x=$(false)
698 do
699 echo while
700 done
701
702 if x=$(false)
703 then
704 echo if
705 fi
706
707 if x=$(true)
708 then
709 echo yes
710 fi
711
712 # Same thing with errexit -- NOT affected
713 set -o errexit
714
715 while x=$(false)
716 do
717 echo while
718 done
719
720 if x=$(false)
721 then
722 echo if
723 fi
724
725 if x=$(true)
726 then
727 echo yes
728 fi
729
730 # Same thing with strict_errexit -- NOT affected
731 shopt -s strict_errexit || true
732
733 while x=$(false)
734 do
735 echo while
736 done
737
738 if x=$(false)
739 then
740 echo if
741 fi
742
743 if x=$(true)
744 then
745 echo yes
746 fi
747
748 ## status: 1
749 ## STDOUT:
750 yes
751 yes
752 ## END
753 ## N-I dash/bash/mksh/ash status: 0
754 ## N-I dash/bash/mksh/ash STDOUT:
755 yes
756 yes
757 yes
758 ## END