Context
While setting up a basic unit testing system, I ran into an odd issue. My goal was to make sure all individual test scripts:
- were run with
set -eto detect errors, without needing to explicitly set this in each file; - knew right away about the functions to be tested (stored in another file) without needing to explicitly source those in each test file.
Observations
Let this be a dummy test file called to-be-sourced.sh. We want to be able to know if a command in it fails:
# Failing command!
false
# Last command is OK:
true
And here is a dummy test runner, which must run the test file:
#! /usr/bin/env bash
if (
set -e
. to-be-sourced.sh
)
then
echo 'Via set: =0'
else
echo 'Via set: ≠0'
fi
This yields Via set: =0, meaning that the runner is happy. But it should not!
My hypothesis was:
set -eis not propagated within.sourcing, and as explained in the help for.andsource, the exit status is the one of the last command.
But then I came up with a workaround that works, but also relies on .:
if bash -ec '. "$0"' to-be-sourced.sh
then
echo 'Via bash: =0'
else
echo 'Via bash: ≠0'
fi
This yields ≠0 whenever a command in the test file fails, regardless of whether that command was the last one of the test file. As a bonus, I can toss any number of . a/library/file.sh within the -c command, so each test file can use all of my functions out of the box. I should therefore be happy, but:
Why does this work, considering that the -c command also relies on . to load the test file (and I thought bash’s -e was equivalent to set’s -e)?
I also thought about using bash’s --init-file, but it appeared to be skipped when a script is passed as a parameter. And anyway my question is not so much about what I was trying to achieve, but rather about the observed difference of behavior.
Edit
Sounds like if is tempering with the way set -e is handled.
This halts execution, indicating failure:
. to-be-sourced.sh
… while this goes into the then (not the else), indicating success:
if . to-be-sourced.sh
then
echo =0
else
echo ≠0
fi
CodePudding user response:
(This may not be precisely correct, but I think it captures what happens.)
In your first example, set -e sets the option in a command that is lexically in the scope of an if statement, and so even though it is set, it is ignored. (You can confirm it is set by running echo $- inside to-be-sourced.sh. Note, too, that . itself has a 0 exit status, which you can confirm by replacing true with an echo statement; it's not that it fails but the failure is ignored.)
In your second example, -e sets the errexit option in a new process, which knows nothing about the if statement and therefore it is not ignored.
