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