| 1 | ---
|
| 2 | default_highlighter: oils-sh
|
| 3 | ---
|
| 4 |
|
| 5 | YSH Fixes Shell's Error Handling (`errexit`)
|
| 6 | ============================================
|
| 7 |
|
| 8 | <style>
|
| 9 | .faq {
|
| 10 | font-style: italic;
|
| 11 | color: purple;
|
| 12 | }
|
| 13 |
|
| 14 | /* copied from web/blog.css */
|
| 15 | .attention {
|
| 16 | text-align: center;
|
| 17 | background-color: #DEE;
|
| 18 | padding: 1px 0.5em;
|
| 19 |
|
| 20 | /* to match p tag etc. */
|
| 21 | margin-left: 2em;
|
| 22 | }
|
| 23 | </style>
|
| 24 |
|
| 25 | YSH is unlike other shells:
|
| 26 |
|
| 27 | - It never silently ignores an error, and it never loses an exit code.
|
| 28 | - There's no reason to write an YSH script without `errexit`, which is on by
|
| 29 | default.
|
| 30 |
|
| 31 | This document explains how YSH makes these guarantees. We first review shell
|
| 32 | error handling, and discuss its fundamental problems. Then we show idiomatic
|
| 33 | YSH code, and look under the hood at the underlying mechanisms.
|
| 34 |
|
| 35 | (If you just want to **use** YSH, see [YSH Error Handling: A Quick
|
| 36 | Guide](ysh-error.html).)
|
| 37 |
|
| 38 | [file a bug]: https://github.com/oilshell/oil/issues
|
| 39 |
|
| 40 | <div id="toc">
|
| 41 | </div>
|
| 42 |
|
| 43 | ## Review of Shell Error Handling Mechanisms
|
| 44 |
|
| 45 | POSIX shell has fundamental problems with error handling. With `set -e` aka
|
| 46 | `errexit`, you're [damned if you do and damned if you don't][bash-faq].
|
| 47 |
|
| 48 | GNU [bash]($xref) fixes some of the problems, but **adds its own**, e.g. with
|
| 49 | respect to process subs, command subs, and assignment builtins.
|
| 50 |
|
| 51 | YSH fixes all the problems by adding new builtin commands, special variables,
|
| 52 | and global options. But you see a simple interface with `try` and `_error`.
|
| 53 |
|
| 54 | Let's review a few concepts before discussing YSH.
|
| 55 |
|
| 56 | ### POSIX Shell
|
| 57 |
|
| 58 | - The special variable `$?` is the exit status of the "last command". It's a
|
| 59 | number between `0` and `255`.
|
| 60 | - If `errexit` is enabled, the shell will abort if `$?` is nonzero.
|
| 61 | - This is subject to the *Disabled `errexit` Quirk*, which I describe below.
|
| 62 |
|
| 63 | These mechanisms are fundamentally incomplete.
|
| 64 |
|
| 65 | ### Bash
|
| 66 |
|
| 67 | Bash improves error handling for pipelines like `ls /bad | wc`.
|
| 68 |
|
| 69 | - `${PIPESTATUS[@]}` stores the exit codes of all processes in a pipeline.
|
| 70 | - When `set -o pipefail` is enabled, `$?` takes into account every process in a
|
| 71 | pipeline.
|
| 72 | - Without this setting, the failure of `ls` would be ignored.
|
| 73 | - `shopt -s inherit_errexit` was introduced in bash 4.4 to re-introduce error
|
| 74 | handling in command sub child processes. This fixes a bash-specific bug.
|
| 75 |
|
| 76 | But there are still places where bash will lose an exit code.
|
| 77 |
|
| 78 |
|
| 79 |
|
| 80 | ## Fundamental Problems
|
| 81 |
|
| 82 | Let's look at **four** fundamental issues with shell error handling. They
|
| 83 | underlie the **nine** [shell pitfalls enumerated in the
|
| 84 | appendix](#list-of-pitfalls).
|
| 85 |
|
| 86 | ### When Is `$?` Set?
|
| 87 |
|
| 88 | Each external process and shell builtin has one exit status. But the
|
| 89 | definition of `$?` is obscure: it's tied to the `pipeline` rule in the POSIX
|
| 90 | shell grammar, which does **not** correspond to a single process or builtin.
|
| 91 |
|
| 92 | We saw that `pipefail` fixes one case:
|
| 93 |
|
| 94 | ls /nonexistent | wc # 2 processes, 2 exit codes, but just one $?
|
| 95 |
|
| 96 | But there are others:
|
| 97 |
|
| 98 | local x=$(false) # 2 exit codes, but just one $?
|
| 99 | diff <(sort left) <(sort right) # 3 exit codes, but just one $?
|
| 100 |
|
| 101 | This issue means that shell scripts fundamentally **lose errors**. The
|
| 102 | language is unreliable.
|
| 103 |
|
| 104 | ### What Does `$?` Mean?
|
| 105 |
|
| 106 | Each process or builtin decides the meaning of its exit status independently.
|
| 107 | Here are two common choices:
|
| 108 |
|
| 109 | 1. **The Failure Paradigm**
|
| 110 | - `0` for success, or non-zero for an error.
|
| 111 | - Examples: most shell builtins, `ls`, `cp`, ...
|
| 112 | 1. **The Boolean Paradigm**
|
| 113 | - `0` for true, `1` for false, or a different number like `2` for an error.
|
| 114 | - Examples: the `test` builtin, `grep`, `diff`, ...
|
| 115 |
|
| 116 | New error handling constructs in YSH deal with this fundamental inconsistency.
|
| 117 |
|
| 118 | ### The Meaning of `if`
|
| 119 |
|
| 120 | Shell's `if` statement tests whether a command exits zero or non-zero:
|
| 121 |
|
| 122 | if grep class *.py; then
|
| 123 | echo 'found class'
|
| 124 | else
|
| 125 | echo 'not found' # is this true?
|
| 126 | fi
|
| 127 |
|
| 128 | So while you'd expect `if` to work in the boolean paradigm, it's closer to
|
| 129 | the failure paradigm. This means that using `if` with certain commands can
|
| 130 | cause the *Error or False Pitfall*:
|
| 131 |
|
| 132 | if grep 'class\(' *.py; then # grep syntax error, status 2
|
| 133 | echo 'found class('
|
| 134 | else
|
| 135 | echo 'not found is a lie'
|
| 136 | fi
|
| 137 | # => grep: Unmatched ( or \(
|
| 138 | # => not found is a lie
|
| 139 |
|
| 140 | That is, the `else` clause conflates grep's **error** status 2 and **false**
|
| 141 | status 1.
|
| 142 |
|
| 143 | Strangely enough, I encountered this pitfall while trying to disallow shell's
|
| 144 | error handling pitfalls in YSH! I describe this in another appendix as the
|
| 145 | "[meta pitfall](#the-meta-pitfall)".
|
| 146 |
|
| 147 | ### Design Mistake: The Disabled `errexit` Quirk
|
| 148 |
|
| 149 | There's more bad news about the design of shell's `if` statement. It's subject
|
| 150 | to the *Disabled `errexit` Quirk*, which means when you use a **shell function**
|
| 151 | in a conditional context, errors are unexpectedly **ignored**.
|
| 152 |
|
| 153 | That is, while `if ls /tmp` is useful, `if my-ls-function /tmp` should be
|
| 154 | avoided. It yields surprising results.
|
| 155 |
|
| 156 | I call this the *`if myfunc` Pitfall*, and show an example in [the
|
| 157 | appendix](#disabled-errexit-quirk-if-myfunc-pitfall).
|
| 158 |
|
| 159 | We can't fix this decades-old bug in shell. Instead we disallow dangerous code
|
| 160 | with `strict_errexit`, and add new error handling mechanisms.
|
| 161 |
|
| 162 |
|
| 163 |
|
| 164 | ## YSH Error Handling: The Big Picture
|
| 165 |
|
| 166 | We've reviewed how POSIX shell and bash work, and showed fundamental problems
|
| 167 | with the shell language.
|
| 168 |
|
| 169 | But when you're using YSH, **you don't have to worry about any of this**!
|
| 170 |
|
| 171 | ### YSH Fails On Every Error
|
| 172 |
|
| 173 | This means you don't have to explicitly check for errors. Examples:
|
| 174 |
|
| 175 | shopt --set ysh:upgrade # Enable good error handling in bin/osh
|
| 176 | # It's the default in bin/ysh.
|
| 177 | shopt --set strict_errexit # Disallow bad shell error handling.
|
| 178 | # Also the default in bin/ysh.
|
| 179 |
|
| 180 | local date=$(date X) # 'date' failure is fatal
|
| 181 | # => date: invalid date 'X'
|
| 182 |
|
| 183 | echo $(date X) # ditto
|
| 184 |
|
| 185 | echo $(date X) $(ls > F) # 'ls' isn't executed; 'date' fails first
|
| 186 |
|
| 187 | ls /bad | wc # 'ls' failure is fatal
|
| 188 |
|
| 189 | diff <(sort A) <(sort B) # 'sort' failure is fatal
|
| 190 |
|
| 191 | On the other hand, you won't experience this problem caused by `pipefail`:
|
| 192 |
|
| 193 | yes | head # doesn't fail due to SIGPIPE
|
| 194 |
|
| 195 | The details are explained below.
|
| 196 |
|
| 197 | ### `try` Handles Command and Expression Errors
|
| 198 |
|
| 199 | You may want to **handle failure** instead of aborting the shell. In this
|
| 200 | case, use the `try` builtin and inspect the `_error` variable it sets.
|
| 201 |
|
| 202 | try { # try takes a block of commands
|
| 203 | ls /etc
|
| 204 | ls /BAD # it stops at the first failure
|
| 205 | ls /lib
|
| 206 | } # After try, $? is always 0
|
| 207 | if (_error.code !== 0) { # Now check _error
|
| 208 | echo 'failed'
|
| 209 | }
|
| 210 |
|
| 211 | Note that:
|
| 212 |
|
| 213 | - The `_error.code` variable is different than `$?`.
|
| 214 | - The leading `_` is a PHP-like convention for special variables /
|
| 215 | "registers" in YSH.
|
| 216 | - Idiomatic YSH programs don't look at `$?`.
|
| 217 |
|
| 218 | You also have fine-grained control over every process in a pipeline:
|
| 219 |
|
| 220 | try {
|
| 221 | ls /bad | wc
|
| 222 | }
|
| 223 | write -- @_pipeline_status # every exit status
|
| 224 |
|
| 225 | And each process substitution:
|
| 226 |
|
| 227 | try {
|
| 228 | diff <(sort left.txt) <(sort right.txt)
|
| 229 | }
|
| 230 | write -- @_process_sub_status # every exit status
|
| 231 |
|
| 232 |
|
| 233 |
|
| 234 |
|
| 235 | <div class="attention">
|
| 236 |
|
| 237 | See [YSH vs. Shell Idioms > Error Handling](idioms.html#error-handling) for
|
| 238 | more examples.
|
| 239 |
|
| 240 | </div>
|
| 241 |
|
| 242 |
|
| 243 |
|
| 244 | Certain expressions produce fatal errors, like:
|
| 245 |
|
| 246 | var x = 42 / 0 # divide by zero will abort shell
|
| 247 |
|
| 248 | The `try` builtin also handles them:
|
| 249 |
|
| 250 | try {
|
| 251 | var x = 42 / 0
|
| 252 | }
|
| 253 | if failed {
|
| 254 | echo 'divide by zero'
|
| 255 | }
|
| 256 |
|
| 257 | More examples:
|
| 258 |
|
| 259 | - Index out of bounds `a[i]`
|
| 260 | - Nonexistent key `d->foo` or `d['foo']`.
|
| 261 |
|
| 262 | Such expression evaluation errors result in status `3`, which is an arbitrary non-zero
|
| 263 | status that's not used by other shells. Status `2` is generally for syntax
|
| 264 | errors and status `1` is for most runtime failures.
|
| 265 |
|
| 266 | ### `boolstatus` Enforces 0 or 1 Status
|
| 267 |
|
| 268 | The `boolstatus` builtin addresses the *Error or False Pitfall*:
|
| 269 |
|
| 270 | if boolstatus grep 'class' *.py { # may abort the program
|
| 271 | echo 'found' # status 0 means 'found'
|
| 272 | } else {
|
| 273 | echo 'not found' # status 1 means 'not found'
|
| 274 | }
|
| 275 |
|
| 276 | Rather than confusing **error** with **false**, `boolstatus` will abort the
|
| 277 | program if `grep` doesn't return 0 or 1.
|
| 278 |
|
| 279 | You can think of this as a shortcut for
|
| 280 |
|
| 281 | try {
|
| 282 | grep 'class' *.py
|
| 283 | }
|
| 284 | case (_error.code) {
|
| 285 | (0) { echo 'found' }
|
| 286 | (1) { echo 'not found' }
|
| 287 | (else) { echo 'fatal'
|
| 288 | exit $[_error.code]
|
| 289 | }
|
| 290 | }
|
| 291 |
|
| 292 | ### FAQ on Language Design
|
| 293 |
|
| 294 | <div class="faq">
|
| 295 |
|
| 296 | Why is there `try` but no `catch`?
|
| 297 |
|
| 298 | </div>
|
| 299 |
|
| 300 | First, it offers more flexibility:
|
| 301 |
|
| 302 | - The handler usually inspects `_error.code`, but it may also inspect
|
| 303 | `_pipeline_status` or `_process_sub_status`.
|
| 304 | - The handler may use `case` instead of `if`, e.g. to distinguish true / false
|
| 305 | / error.
|
| 306 |
|
| 307 | Second, it makes the language smaller:
|
| 308 |
|
| 309 | - `try` / `catch` would require specially parsed keywords. But our `try` is a
|
| 310 | shell builtin that takes a block, like `cd` or `shopt`.
|
| 311 | - The builtin also lets us write either `try ls` or `try { ls }`, which is hard
|
| 312 | with a keyword.
|
| 313 |
|
| 314 | Another way to remember this is that there are **three parts** to handling an
|
| 315 | error, each of which has independent choices:
|
| 316 |
|
| 317 | 1. Does `try` take a simple command or a block? For example, `try ls` versus
|
| 318 | `try { ls; var x = 42 / n }`
|
| 319 | 2. Which status do you want to inspect?
|
| 320 | 3. Inspect it with `if` or `case`? As mentioned, `boolstatus` is a special
|
| 321 | case of `try / case`.
|
| 322 |
|
| 323 | <div class="faq">
|
| 324 |
|
| 325 | Why is `_error.code` different from `$?`
|
| 326 |
|
| 327 | </div>
|
| 328 |
|
| 329 | This avoids special cases in the interpreter for `try`, which is again a
|
| 330 | builtin that takes a block.
|
| 331 |
|
| 332 | The exit status of `try` is always `0`. If it returned a non-zero status, the
|
| 333 | `errexit` rule would trigger, and you wouldn't be able to handle the error!
|
| 334 |
|
| 335 | Generally, [errors occur *inside* blocks, not
|
| 336 | outside](proc-block-func.html#errors).
|
| 337 |
|
| 338 | Again, idiomatic YSH scripts never look at `$?`, which is only used to trigger
|
| 339 | shell's `errexit` rule. Instead they invoke `try` and inspect `_error.code`
|
| 340 | when they want to handle errors.
|
| 341 |
|
| 342 | <div class="faq">
|
| 343 |
|
| 344 | Why `boolstatus`? Can't you just change what `if` means in YSH?
|
| 345 |
|
| 346 | </div>
|
| 347 |
|
| 348 | I've learned the hard way that when there's a shell **semantics** change, there
|
| 349 | must be a **syntax** change. In general, you should be able to read code on
|
| 350 | its own, without context.
|
| 351 |
|
| 352 | Readers shouldn't have to constantly look up whether `ysh:upgrade` is on. There
|
| 353 | are some cases where this is necessary, but it should be minimized.
|
| 354 |
|
| 355 | Also, both `if foo` and `if boolstatus foo` are useful in idiomatic YSH code.
|
| 356 |
|
| 357 |
|
| 358 |
|
| 359 | <div class="attention">
|
| 360 |
|
| 361 | **Most users can skip to [the summary](#summary).** You don't need to know all
|
| 362 | the details to use YSH.
|
| 363 |
|
| 364 | </div>
|
| 365 |
|
| 366 |
|
| 367 |
|
| 368 | ## Reference: Global Options
|
| 369 |
|
| 370 |
|
| 371 | Under the hood, we implement the `errexit` option from POSIX, bash options like
|
| 372 | `pipefail` and `inherit_errexit`, and add more options of our
|
| 373 | own. They're all hidden behind [option groups](options.html) like `strict:all`
|
| 374 | and `ysh:upgrade`.
|
| 375 |
|
| 376 | The following sections explain new YSH options.
|
| 377 |
|
| 378 | ### `command_sub_errexit` Adds More Errors
|
| 379 |
|
| 380 | In all Bourne shells, the status of command subs is lost, so errors are ignored
|
| 381 | (details in the [appendix](#quirky-behavior-of)). For example:
|
| 382 |
|
| 383 | echo $(date X) $(date Y) # 2 failures, both ignored
|
| 384 | echo # program continues
|
| 385 |
|
| 386 | The `command_sub_errexit` option makes both `date` invocations an an error.
|
| 387 | The status `$?` of the parent `echo` command will be `1`, so if `errexit` is
|
| 388 | on, the shell will abort.
|
| 389 |
|
| 390 | (Other shells should implement `command_sub_errexit`!)
|
| 391 |
|
| 392 | ### `process_sub_fail` Is Analogous to `pipefail`
|
| 393 |
|
| 394 | Similarly, in this example, `sort` will fail if the file doesn't exist.
|
| 395 |
|
| 396 | diff <(sort left.txt) <(sort right.txt) # any failures are ignored
|
| 397 |
|
| 398 | But there's no way to see this error in bash. YSH adds `process_sub_fail`,
|
| 399 | which folds the failure into `$?` so `errexit` can do its job.
|
| 400 |
|
| 401 | You can also inspect the special `_process_sub_status` array variable to
|
| 402 | implement custom error logic.
|
| 403 |
|
| 404 | ### `strict_errexit` Flags Two Problems
|
| 405 |
|
| 406 | Like other `strict_*` options, YSH `strict_errexit` improves your shell
|
| 407 | programs, even if you run them under another shell like [bash]($xref)! It's
|
| 408 | like a linter *at runtime*, so it can catch things that [ShellCheck][] can't.
|
| 409 |
|
| 410 | [ShellCheck]: https://www.shellcheck.net/
|
| 411 |
|
| 412 | `strict_errexit` disallows code that exhibits these problems:
|
| 413 |
|
| 414 | 1. The `if myfunc` Pitfall
|
| 415 | 1. The `local x=$(false)` Pitfall
|
| 416 |
|
| 417 | See the appendix for examples of each.
|
| 418 |
|
| 419 | #### Rules to Prevent the `if myfunc` Pitfall
|
| 420 |
|
| 421 | In any conditional context, `strict_errexit` disallows:
|
| 422 |
|
| 423 | 1. All commands except `((`, `[[`, and some simple commands (e.g. `echo foo`).
|
| 424 | - Detail: `! ls` is considered a pipeline in the shell grammar. We have to
|
| 425 | allow it, while disallowing `ls | grep foo`.
|
| 426 | 2. Function/proc invocations (which are a special case of simple
|
| 427 | commands.)
|
| 428 | 3. Command sub and process sub (`shopt --unset allow_csub_psub`)
|
| 429 |
|
| 430 | This means that you should check the exit status of functions and pipeline
|
| 431 | differently. See [Does a Function
|
| 432 | Succeed?](idioms.html#does-a-function-succeed), [Does a Pipeline
|
| 433 | Succeed?](idioms.html#does-a-pipeline-succeed), and other [YSH vs. Shell
|
| 434 | Idioms](idioms.html).
|
| 435 |
|
| 436 | #### Rule to Prevent the `local x=$(false)` Pitfall
|
| 437 |
|
| 438 | - Command Subs and process subs are disallowed in assignment builtins: `local`,
|
| 439 | `declare` aka `typeset`, `readonly`, and `export`.
|
| 440 |
|
| 441 | No:
|
| 442 |
|
| 443 | local x=$(false)
|
| 444 |
|
| 445 | Yes:
|
| 446 |
|
| 447 | var x = $(false) # YSH style
|
| 448 |
|
| 449 | local x # Shell style
|
| 450 | x=$(false)
|
| 451 |
|
| 452 | ### `sigpipe_status_ok` Ignores an Issue With `pipefail`
|
| 453 |
|
| 454 | When you turn on `pipefail`, you may inadvertently run into this behavior:
|
| 455 |
|
| 456 | yes | head
|
| 457 | # => y
|
| 458 | # ...
|
| 459 |
|
| 460 | echo ${PIPESTATUS[@]}
|
| 461 | # => 141 0
|
| 462 |
|
| 463 | That is, `head` closes the pipe after 10 lines, causing the `yes` command to
|
| 464 | **fail** with `SIGPIPE` status `141`.
|
| 465 |
|
| 466 | This error shouldn't be fatal, so OSH has a `sigpipe_status_ok` option, which
|
| 467 | is on by default in YSH.
|
| 468 |
|
| 469 | ### `verbose_errexit`
|
| 470 |
|
| 471 | When `verbose_errexit` is on, the shell prints errors to `stderr` when the
|
| 472 | `errexit` rule is triggered.
|
| 473 |
|
| 474 | ### FAQ on Options
|
| 475 |
|
| 476 | <div class="faq">
|
| 477 |
|
| 478 | Why is there no `_command_sub_status`? And why is `command_sub_errexit` named
|
| 479 | differently than `process_sub_fail` and `pipefail`?
|
| 480 |
|
| 481 | </div>
|
| 482 |
|
| 483 | Command subs are executed **serially**, while process subs and pipeline parts
|
| 484 | run **in parallel**.
|
| 485 |
|
| 486 | So a command sub can "abort" its parent command, setting `$?` immediately.
|
| 487 | The parallel constructs must wait until all parts are done and save statuses in
|
| 488 | an array. Afterward, they determine `$?` based on the value of `pipefail` and
|
| 489 | `process_sub_fail`.
|
| 490 |
|
| 491 | <div class="faq">
|
| 492 |
|
| 493 | Why are `strict_errexit` and `command_sub_errexit` different options?
|
| 494 |
|
| 495 | </div>
|
| 496 |
|
| 497 | Because `shopt --set strict:all` can be used to improve scripts that are run
|
| 498 | under other shells like [bash]($xref). It's like a runtime linter that
|
| 499 | disallows dangerous constructs.
|
| 500 |
|
| 501 | On the other hand, if you write code with `command_sub_errexit` on, it's
|
| 502 | impossible to get the same failures under bash. So `command_sub_errexit` is
|
| 503 | not a `strict_*` option, and it's meant for code that runs only under YSH.
|
| 504 |
|
| 505 | <div class="faq">
|
| 506 |
|
| 507 | What's the difference between bash's `inherit_errexit` and YSH
|
| 508 | `command_sub_errexit`? Don't they both relate to command subs?
|
| 509 |
|
| 510 | </div>
|
| 511 |
|
| 512 | - `inherit_errexit` enables failure in the **child** process running the
|
| 513 | command sub.
|
| 514 | - `command_sub_errexit` enables failure in the **parent** process, after the
|
| 515 | command sub has finished.
|
| 516 |
|
| 517 |
|
| 518 |
|
| 519 | ## Summary
|
| 520 |
|
| 521 | YSH uses three mechanisms to fix error handling once and for all.
|
| 522 |
|
| 523 | It has two new **builtins** that relate to errors:
|
| 524 |
|
| 525 | 1. `try` lets you explicitly handle errors when `errexit` is on.
|
| 526 | 1. `boolstatus` enforces a true/false meaning. (This builtin is less common).
|
| 527 |
|
| 528 | It has three **special variables**:
|
| 529 |
|
| 530 | 1. The `_error` register, which is set by `try`.
|
| 531 | - Remember that `_error.code` is distinct from `$?`, and that idiomatic YSH
|
| 532 | programs don't use `$?`.
|
| 533 | 1. The `_pipeline_status` array (another name for bash's `PIPESTATUS`)
|
| 534 | 1. The `_process_sub_status` array for process substitutions.
|
| 535 |
|
| 536 | Finally, it supports all of these **global options**:
|
| 537 |
|
| 538 | - From POSIX shell:
|
| 539 | - `errexit`
|
| 540 | - From [bash]($xref):
|
| 541 | - `pipefail`
|
| 542 | - `inherit_errexit` aborts the child process of a command sub.
|
| 543 | - New:
|
| 544 | - `command_sub_errexit` aborts the parent process immediately after a failed
|
| 545 | command sub.
|
| 546 | - `process_sub_fail` is analogous to `pipefail`.
|
| 547 | - `strict_errexit` flags two common problems.
|
| 548 | - `sigpipe_status_ok` ignores a spurious "broken pipe" failure.
|
| 549 | - `verbose_errexit` controls whether error messages are printed.
|
| 550 |
|
| 551 | When using `bin/osh`, set all options at once with `shopt --set ysh:upgrade
|
| 552 | strict:all`. Or use `bin/ysh`, where they're set by default.
|
| 553 |
|
| 554 | <!--
|
| 555 | Related 2020 blog post [Reliable Error
|
| 556 | Handling](https://www.oilshell.org/blog/2020/10/osh-features.html#reliable-error-handling).
|
| 557 | -->
|
| 558 |
|
| 559 |
|
| 560 | ## Related Docs
|
| 561 |
|
| 562 | - [YSH vs. Shell Idioms](idioms.html) shows more examples of `try` and `boolstatus`.
|
| 563 | - [Shell Idioms](shell-idioms.html) has a section on fixing `strict_errexit`
|
| 564 | problems in Bourne shell.
|
| 565 |
|
| 566 | Good articles on `errexit`:
|
| 567 |
|
| 568 | - Bash FAQ: [Why doesn't `set -e` do what I expected?][bash-faq]
|
| 569 | - [Bash: Error Handling](http://fvue.nl/wiki/Bash:_Error_handling) from
|
| 570 | `fvue.nl`
|
| 571 |
|
| 572 | [bash-faq]: http://mywiki.wooledge.org/BashFAQ/105
|
| 573 |
|
| 574 | Spec Test Suites:
|
| 575 |
|
| 576 | - <https://www.oilshell.org/release/latest/test/spec.wwz/survey/errexit.html>
|
| 577 | - <https://www.oilshell.org/release/latest/test/spec.wwz/survey/errexit-oil.html>
|
| 578 |
|
| 579 | These docs aren't about error handling, but they're also painstaking
|
| 580 | backward-compatible overhauls of shell!
|
| 581 |
|
| 582 | - [Simple Word Evaluation in Unix Shell](simple-word-eval.html)
|
| 583 | - [Egg Expressions (YSH Regexes)](eggex.html)
|
| 584 |
|
| 585 | For reference, this work on error handling was described in [Four Features That
|
| 586 | Justify a New Unix
|
| 587 | Shell](https://www.oilshell.org/blog/2020/10/osh-features.html) (October 2020).
|
| 588 | Since then, we changed `try` and `_error` to be more powerful and general.
|
| 589 |
|
| 590 |
|
| 591 |
|
| 592 | ## Appendices
|
| 593 |
|
| 594 | ### List Of Pitfalls
|
| 595 |
|
| 596 | We mentioned some of these pitfalls:
|
| 597 |
|
| 598 | 1. The `if myfunc` Pitfall, caused by the Disabled `errexit` Quirk (`strict_errexit`)
|
| 599 | 1. The `local x=$(false)` Pitfall (`strict_errexit`)
|
| 600 | 1. The Error or False Pitfall (`boolstatus`, `try` / `case`)
|
| 601 | - Special case: When the child process is another instance of the shell, the
|
| 602 | Meta Pitfall is possible.
|
| 603 | 1. The Process Sub Pitfall (`process_sub_fail` and `_process_sub_status`)
|
| 604 | 1. The `yes | head` Pitfall (`sigpipe_status_ok`)
|
| 605 |
|
| 606 | There are two pitfalls related to command subs:
|
| 607 |
|
| 608 | 6. The `echo $(false)` Pitfall (`command_sub_errexit`)
|
| 609 | 6. Bash's `inherit_errexit` pitfall.
|
| 610 | - As mentioned, this bash 4.4 option fixed a bug in earlier versions of
|
| 611 | bash. YSH reimplements it and turns it on by default.
|
| 612 |
|
| 613 | Here are two more pitfalls that don't require changes to YSH:
|
| 614 |
|
| 615 | 8. The Trailing `&&` Pitfall
|
| 616 | - When `test -d /bin && echo found` is at the end of a function, the exit
|
| 617 | code is surprising.
|
| 618 | - Solution: always use `if` rather than `&&`.
|
| 619 | - More reasons: the `if` is easier to read, and `&&` isn't useful when
|
| 620 | `errexit` is on.
|
| 621 | 8. The surprising return value of `(( i++ ))`, `let`, `expr`, etc.
|
| 622 | - Solution: Use `i=$((i + 1))`, which is valid POSIX shell.
|
| 623 | - In YSH, use `setvar i += 1`.
|
| 624 |
|
| 625 | #### Example of `inherit_errexit` Pitfall
|
| 626 |
|
| 627 | In bash, `errexit` is disabled in command sub child processes:
|
| 628 |
|
| 629 | set -e
|
| 630 | shopt -s inherit_errexit # needed to avoid 'touch two'
|
| 631 | echo $(touch one; false; touch two)
|
| 632 |
|
| 633 | Without the option, it will touch both files, even though there is a failure
|
| 634 | `false` after the first.
|
| 635 |
|
| 636 | #### Bash has a grammatical quirk with `set -o failglob`
|
| 637 |
|
| 638 | This isn't a pitfall, but a quirk that also relates to errors and shell's
|
| 639 | **grammar**. Recall that the definition of `$?` is tied to the grammar.
|
| 640 |
|
| 641 | Consider this program:
|
| 642 |
|
| 643 | set -o failglob
|
| 644 | echo *.ZZ # no files match
|
| 645 | echo status=$? # show failure
|
| 646 | # => status=1
|
| 647 |
|
| 648 | This is the same program with a newline replaced by a semicolon:
|
| 649 |
|
| 650 | set -o failglob
|
| 651 |
|
| 652 | # Surprisingly, bash doesn't execute what's after ;
|
| 653 | echo *.ZZ; echo status=$?
|
| 654 | # => (no output)
|
| 655 |
|
| 656 | But it behaves differently. This is because newlines and semicolons are handled
|
| 657 | in different **productions of the grammar**, and produce distinct syntax trees.
|
| 658 |
|
| 659 | (A related quirk is that this same difference can affect the number of
|
| 660 | processes that shells start!)
|
| 661 |
|
| 662 | ### Disabled `errexit` Quirk / `if myfunc` Pitfall
|
| 663 |
|
| 664 | This quirk is a bad interaction between the `if` statement, shell functions,
|
| 665 | and `errexit`. It's a **mistake** in the design of the shell language.
|
| 666 | Example:
|
| 667 |
|
| 668 | set -o errexit # don't ignore errors
|
| 669 |
|
| 670 | myfunc() {
|
| 671 | ls /bad # fails with status 1
|
| 672 | echo 'should not get here'
|
| 673 | }
|
| 674 |
|
| 675 | myfunc # Good: script aborts before echo
|
| 676 | # => ls: '/bad': no such file or directory
|
| 677 |
|
| 678 | if myfunc; then # Surprise! It behaves differently in a condition.
|
| 679 | echo OK
|
| 680 | fi
|
| 681 | # => ls: '/bad': no such file or directory
|
| 682 | # => should not get here
|
| 683 |
|
| 684 | We see "should not get here" because the shell **silently disables** `errexit`
|
| 685 | while executing the condition of `if`. This relates to the fundamental
|
| 686 | problems above:
|
| 687 |
|
| 688 | 1. Does the function use the failure paradigm or the boolean paradigm?
|
| 689 | 2. `if` tests a single exit status, but every command in a function has an exit
|
| 690 | status. Which one should we consider?
|
| 691 |
|
| 692 | This quirk occurs in all **conditional contexts**:
|
| 693 |
|
| 694 | 1. The condition of the `if`, `while`, and `until` constructs
|
| 695 | 2. A command/pipeline prefixed by `!` (negation)
|
| 696 | 3. Every clause in `||` and `&&` except the last.
|
| 697 |
|
| 698 | ### The Meta Pitfall
|
| 699 |
|
| 700 | I encountered the *Error or False Pitfall* while trying to disallow other error
|
| 701 | handling pitfalls! The *meta pitfall* arises from a combination of the issues
|
| 702 | discussed:
|
| 703 |
|
| 704 | 1. The `if` statement tests for zero or non-zero status.
|
| 705 | 1. The condition of an `if` may start child processes. For example, in `if
|
| 706 | myfunc | grep foo`, the `myfunc` invocation must be run in a subshell.
|
| 707 | 1. You may want an external process to use the **boolean paradigm**, and
|
| 708 | that includes **the shell itself**. When any of the `strict_` options
|
| 709 | encounters bad code, it aborts the shell with **error** status `1`, not
|
| 710 | boolean **false** `1`.
|
| 711 |
|
| 712 | The result of this fundamental issue is that `strict_errexit` is quite strict.
|
| 713 | On the other hand, the resulting style is straightforward and explicit.
|
| 714 | Earlier attempts allowed code that is too subtle.
|
| 715 |
|
| 716 | ### Quirky Behavior of `$?`
|
| 717 |
|
| 718 | This is a different way of summarizing the information above.
|
| 719 |
|
| 720 | Simple commands have an obvious behavior:
|
| 721 |
|
| 722 | echo hi # $? is 0
|
| 723 | false # $? is 1
|
| 724 |
|
| 725 | But the parent process loses errors from failed command subs:
|
| 726 |
|
| 727 | echo $(false) # $? is 0
|
| 728 | # YSH makes it fail with command_sub_errexit
|
| 729 |
|
| 730 | Surprisingly, bare assignments take on the value of any command subs:
|
| 731 |
|
| 732 | x=$(false) # $? is 1 -- we did NOT lose the exit code
|
| 733 |
|
| 734 | But assignment builtins have the problem again:
|
| 735 |
|
| 736 | local x=$(false) # $? is 0 -- exit code is clobbered
|
| 737 | # disallowed by YSH strict_errexit
|
| 738 |
|
| 739 | So shell is confusing and inconsistent, but YSH fixes all these problems. You
|
| 740 | never lose the exit code of `false`.
|
| 741 |
|
| 742 |
|
| 743 |
|
| 744 |
|
| 745 | ## Acknowledgments
|
| 746 |
|
| 747 | - Thank you to `ca2013` for extensive review and proofreading of this doc.
|
| 748 |
|
| 749 |
|