r/bash • • 13h ago

set -e silently does nothing inside a function called from an if, and I only found out in production

14 Upvotes

I had a cleanup script that always started with the usual set -euo pipefail. One day it reported success while half the work hadn’t happened. Here’s a stripped-down version of what bit me:

#!/usr/bin/env bash
set -euo pipefail

cleanup_old() {
  rm "$1/old.log"        # fails if the file is missing
  echo "cleaned $1"
}

if cleanup_old /tmp/nope; then
  echo "all good"
fi

Output:

rm: cannot remove '/tmp/nope/old.log': No such file or directory
cleaned /tmp/nope
all good

The rm fails, but the function keeps going, and the echo at the end gives it a zero exit status. The bash manual says -e is ignored for any command in a context where the exit status is being tested (if, while, &&/|| lists, !). That also applies to everything inside a function called from such a context. set -e at the top of the file doesn’t help, and nothing warns you.

What I do now:

cleanup_old() {
  rm "$1/old.log" || return 1
  echo "cleaned $1"
}

The explicit || return is noisier, but it works regardless of how the function gets called. For anything where partial success is dangerous, I also stopped using functions as if conditions and run them as plain statements, checking $? or using || on the call site deliberately.

Related one that bit me the same week: local out=$(some_cmd) always returns the exit status of local, not some_cmd. Splitting it into local out; out=$(some_cmd) fixes it.

I’ve been reading a lot of generated and copy-pasted scripts lately, and nearly all of them treat set -euo pipefail as a safety net. After this, I think of it as a default that works in the simple cases and quietly stops working in exactly the places where functions get composed.

For those who write larger bash scripts: do you rely on set -e at all, or do you handle errors explicitly everywhere and keep -e only as a backstop?