Nathan Kleyn

Scala by day — Rust, Ruby and Haskell by night. I hail from London.

Hi! I currently work for Intent HQ as Director of Engineering, writing functional Scala by day and Rust by night. I write for SitePoint about Ruby, and I’m on Twitter and on GitHub. You can get in touch with me at mail/at/nathankleyn.com.


The Surpring Behaviour Of `set -e` In Shell Functions

08 Mar 2021

Today I just learned something about shell/bash that I never knew before which caused me quite a bit of confusion.

If you have set -e on in a bash/shell script, as you probably know the shell will exit if any line has a non-zero exit status and it isn’t caught and handled by something.

But what I did not know before today was that this is “weird” inside functions:

set -e

foo () {
    echo "I should be printed first!"
    false
    echo "I should not be printed?"
}

foo || echo "I should be printed last!"

Maybe somewhat surprisingly the output of this is:

I should be printed first!
I should not be printed?

This happens because if you handle the output of an entire function then it kind of acts like every line inside the function is also handled. This means lines can run that you were not expecting!

Additionally, the last line that runs will be the exit status of the function, so in this case being echo my “I should be printed last!” line didn’t even run because echo always returns a zero exit status!

Therefore, it’s more correct to do:

set -eo pipefail

foo () {
    echo "I should be printed first!"
    false || echo "I should be printed last!" && return 1
    echo "I should not be printed?"
}

foo

That is, handle each line failing where it happens, and don’t try to handle whole functions.

This is documented in some of the various versions of documentation for set -e:

-e errexit

Exit immediately if any untested command fails in non-interactive mode. The exit status of a command is considered to be explicitly tested if the command is part of the list used to control an if, elif, while, or until; if the command is the left hand operand of an “&&” or “||” operator; or if the command is a pipeline preceded by the ! operator. If a shell function is executed and its exit status is explicitly tested, all commands of the function are considered to be tested as well.

But I think this is almost certainly surprising behaviour for most users of shell.