top | item 27164388

(no title)

twooster | 4 years ago

You should almost always be running Bash in `-e` (exit-on-error) mode. This necessitates precisely the construct mentioned in this article.

For example:

  set -e

  var="$( false )"
  if [ $? -eq 0 ] ; then
    echo Ok: "$var"
  else
    echo Not ok: $?
  fi
If you run this program, neither "Ok" or "Not ok" will be echoed, because the program will exit with an error on the `var=` line. (Not to mention the $? on the not-ok line won't work because it will be the exit code of the `[` test command in the conditional, not the exit code of the captured subshell command).

Instead the following will work:

  set -e

  if var="$( false )" ; then
    echo Ok: "$var"
  else
    echo Not ok: $?
  fi
Note that this will _not_ work:

  if ! var="$( false )"; then
    echo Not ok: $?
  fi
Your output will be "Not ok: 0". This is because negation impacts the exit code of the previous command.

discuss

order

chubot|4 years ago

No that is dangerous, consider this:

    set -e
    myfunc() {
      date %x  # syntax error; returns 1, should be +%x
      echo 'should not get here'
    }

    if var=$(myfunc); then
      echo $var   # erroneously prints 'should not get here'
    else
      echo failed
    fi
Then you will ignore failure, which is bad.

This is a variant of the issue that the sibling comment brought up -- error handling is disabled inside "if" conditions.

In Oil the whole construct is unconditionally disabled by strict_errexit. It's too subtle.

Oil has 2 ways of capturing the exit code, including

    run --assign :status -- mycommand  # exit 0 but assign the status to a var
and

    shopt --unset errexit {  # explicitly disable error handling if you want
      mycmd
      var status = $?
    }

I'm looking for feedback to make sure that Oil has indeed fixed all of this stuff: https://www.oilshell.org/blog/2020/10/osh-features.html

Basically the situation is "damned if you do and damned if you don't" in Bourne shell, so you need language/interpreter changes to really fix it. The rules are too tricky to remember even for shell experts -- there are persistent arguments on POSIX behavior that is over 20 years old, simply because it's so confusing.

https://github.com/oilshell/oil/wiki/Where-To-Send-Feedback

xyzzy_plugh|4 years ago

> The rules are too tricky to remember even for shell experts -- there are persistent arguments on POSIX behavior that is over 20 years old, simply because it's so confusing.

I don't know, I find it easier to not use set -e. I find it significantly easier to just explicitly handle all my errors. Having my script exit at some arbitrary point is almost never desirable.

I find chaining && and || pretty intuitive.

  var=$(myfunc) &&
    echo OK ||
    {
      echo Not OK
      exit 1
    }
This is pretty contrived. I'd probably put the error handling in a function and then only handle the failure scenario:

  var=$(myfunc) || die 'Not OK'
  echo OK
I never run into problems, this always works as expected, I don't need any language or interpreter changes to fix it. Once you realize `if` is just syntactic sugar and [ is just `test` then the world gets pretty simple.

jaytaylor|4 years ago

That is a pretty subtle and nasty sharp edge! I consider myself quite proficient with bash and best practices, and it still took me a moment to think through this to understand how where things went 'wrong' in your example.

Thanks for your work on Oil shell, I don't yet use it regularly but hope it becomes mainstream. I'm definitely rooting for you, chubot!

andsens|4 years ago

It's worse than that actually, `set -e`/"errexit" is disabled for function calls in ifs. Meaning this:

    set -e
    fn() {
        false
        echo "didn't exit"
    }
    if fn; then
        echo "fn() succeeded"
    fi
will output

    didn't exit
    fn() succeeded

jbrot|4 years ago

Yep, likewise it’s also disabled in function calls and sub shells that are invoked in an && or || block (i.e., in the above case of you change the if statement to “fn && echo ...” you’ll see the same behavior).

Even worse, you can add the line “set -e” inside the function explicitly re-enabling it and it still won’t change the outcome because errexit wasn’t technically unset!

matvore|4 years ago

`set -e` has a couple of surprising corner cases and in the details is pretty hard to understand. The documentation in `man bash` for the `set -e` flag is 28 lines in my terminal, and the other flags are 2 or 3 lines.

One such corner case is in pipelines. Only the last command in a pipeline can cause the script to terminate.

Another corner case is `foo && bar`, often used as an abbreviated `if`, will not exit when `foo` fails.

It is not a significant task to just add `|| exit $?` after any command whose failure should cause an abort.

mgerdts|4 years ago

My scripts tend to start with

set -euo pipefail

This addresses the pipeline case you mention and also notices use of unitialized variables.

Dr_Emann|4 years ago

For simple cases, I'll do that, but for longer commands, I've taken to doing

rc=0 long command || rc = $? if (( rc == 0 ))...