1 | #!/usr/bin/env bash
|
2 | #
|
3 | # Prep for the "Ultimate Guide to errexit".
|
4 | #
|
5 | # Usage:
|
6 | # ./errexit-pitfalls.sh <function name>
|
7 |
|
8 | set -o nounset
|
9 | set -o pipefail
|
10 | set -o errexit
|
11 |
|
12 | die() {
|
13 | echo "$@" >&2
|
14 | exit 1
|
15 | }
|
16 |
|
17 | #
|
18 | # inherit_errexit (bash and Oil)
|
19 | #
|
20 | # It's confusing that command subs clear the errexit flag (but subshells
|
21 | # don't.)
|
22 |
|
23 | # This is a bash quirk
|
24 | command-sub-needs-inherit-errexit() {
|
25 | echo $(echo 1; false; echo 2)
|
26 |
|
27 | echo 'inherit_errexit'
|
28 | shopt -s inherit_errexit || die "bash 4.4 required"
|
29 |
|
30 | echo $(echo 1; false; echo 2)
|
31 | }
|
32 |
|
33 | subshell-demo() {
|
34 | ( echo 1; false; echo 2 ) # prints 1
|
35 | }
|
36 |
|
37 | #
|
38 | # command_sub_errexit (Oil)
|
39 | #
|
40 | # It's confusing that a=$(false) is different than local a=$(false).
|
41 |
|
42 | assignment-builtin-overwrites-status() {
|
43 | set +o errexit
|
44 |
|
45 | a=$(false)
|
46 | echo $? # this is 1
|
47 |
|
48 | local b=$(false)
|
49 | echo $? # surprisingly, it's 0!
|
50 | }
|
51 |
|
52 | oil-more-errexit() {
|
53 | shopt -s command_sub_errexit
|
54 |
|
55 | local b=$(false) # FAILS!
|
56 | echo $?
|
57 | }
|
58 |
|
59 | #
|
60 | # strict_errexit (Oil)
|
61 | #
|
62 | # It's confusing that 'if myfunc', 'while/until myfunc', 'myfunc || die',
|
63 | # 'myfunc && echo OK' and '! myfunc' change errexit.
|
64 |
|
65 | myfunc() {
|
66 | echo '--- myfunc'
|
67 | ls /zz # should cause failure
|
68 | echo "shouldn't get here"
|
69 | }
|
70 |
|
71 | proper-function-failure() {
|
72 | # Proper failure
|
73 | myfunc
|
74 | echo "Doesn't get here"
|
75 | }
|
76 |
|
77 | # Function calls in condition cause the function to IGNORE FAILURES
|
78 | function-call-in-condition() {
|
79 | # All 4 of these surprisingly don't fail
|
80 |
|
81 | if myfunc; then
|
82 | echo 'if'
|
83 | fi
|
84 |
|
85 | myfunc && echo '&&'
|
86 |
|
87 | myfunc || echo 'not printed'
|
88 |
|
89 | ! myfunc
|
90 | }
|
91 |
|
92 | proper-lastpipe-failure() {
|
93 | { echo hi; exit 5; } | sort
|
94 | echo "doesn't get here"
|
95 | }
|
96 |
|
97 | # Same problem for pipelines, another compound command.
|
98 | pipeline-in-conditionals() {
|
99 | # If the above function aborts early, then this one should too.
|
100 | if { echo hi; exit 5; } | sort; then
|
101 | echo true
|
102 | else
|
103 | echo false
|
104 | fi
|
105 | echo bad
|
106 | }
|
107 |
|
108 | #
|
109 | # Conditional As Last Statement in Function Pitfall
|
110 | #
|
111 | # It's confusing that calling a one-line function with 'foo && echo OK' isn't
|
112 | # the same as inlining that statement (due to differing exit codes).
|
113 |
|
114 |
|
115 | # Possible strict_errexit rule:
|
116 | # Disallow && (an AndOr with && as one of the operators) unless it's in an
|
117 | # if/while/until condition.
|
118 | # This would require an extra flag for _Execute().
|
119 | # cmd_flags | ERREXIT (to avoid the stack)
|
120 | # cmd_flags | IS_CONDITION
|
121 |
|
122 | last-func() {
|
123 | test -d nosuchdir && echo no dir
|
124 | echo survived
|
125 |
|
126 | set -e
|
127 | f() { test -d nosuchdir && echo no dir; }
|
128 | echo 'in function'
|
129 | f
|
130 |
|
131 | # We do NOT get here.
|
132 | echo survived
|
133 | }
|
134 |
|
135 |
|
136 | #
|
137 | # Builtins
|
138 | #
|
139 |
|
140 | read-exit-status() {
|
141 | set +o errexit
|
142 |
|
143 | echo line > _tmp/line
|
144 | read x < _tmp/line # status 0 as expected
|
145 | echo status=$?
|
146 |
|
147 | echo -n no-newline > _tmp/no
|
148 | read x < _tmp/no # somewhat surprising status 1, because no delimiter read
|
149 | # This is for terminating loops?
|
150 |
|
151 | echo status=$?
|
152 |
|
153 | # Solution: Oil can have its own builtin. It already has 'getline'.
|
154 | # getfile / slurp / readall
|
155 | # readall :x < myfile
|
156 | #
|
157 | # grep foo *.c | readall :results
|
158 | #
|
159 | # grep foo *.c | slurp :results # I Kind of like this
|
160 | #
|
161 | # Unlike $(echo hi), it includes the newline
|
162 | # Unlike read x, it doesn't fail if there's NO newline.
|
163 | }
|
164 |
|
165 |
|
166 | #
|
167 | # lastpipe and SIGPIPE
|
168 | #
|
169 |
|
170 | # This is a bit of trivia about the exit status.
|
171 | sigpipe-error() {
|
172 | set +o errexit
|
173 | busybox | head -n 1
|
174 | echo status=$? # 141 because of sigpipe
|
175 |
|
176 | # Workaround
|
177 | { busybox || true; } | head -n 1
|
178 | echo status=$?
|
179 | }
|
180 |
|
181 | #
|
182 | # Other constructs I don't care about from https://mywiki.wooledge.org/BashFAQ/105
|
183 | #
|
184 |
|
185 | #
|
186 | # - let i++
|
187 | # - (( i++ ))
|
188 |
|
189 | # So we DO want command_sub_errexit. Because we don't want
|
190 | #
|
191 | # diff $(error 1) $(error 2)
|
192 | #
|
193 | # to execute the diff! It should fail earlier
|
194 |
|
195 | word-failures() {
|
196 | set -o errexit
|
197 |
|
198 | echo $BASH_VERSION
|
199 | #shopt -s inherit_errexit
|
200 |
|
201 | echo "command sub $(false)"
|
202 | echo status=$?
|
203 |
|
204 | readonly x="readonly command sub $(false)"
|
205 | echo status=$?
|
206 |
|
207 | [[ "dparen $(false)" == 'dparen ' ]]
|
208 | echo status=$?
|
209 |
|
210 | (( a = $(false)42 ))
|
211 | echo status=$?
|
212 | echo a=$a
|
213 |
|
214 | diff -u <(cat nonexistent.txt) /dev/null
|
215 | echo status=$?
|
216 | }
|
217 |
|
218 | "$@"
|
219 |
|