OILS / spec / errexit-osh.test.sh View on Github | oils.pub

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