| 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 |  |