John Calcote John Calcote - 1 year ago 68
Bash Question

Why does eval exit subshell mid-&& with set -e?

Why does bash do what I'd expect here with a compound command in a subshell:

$ bash -x -c 'set -e; (false && true; echo hi); echo here'
+ set -e
+ false
+ echo hi
+ echo here

But NOT do what I'd expect here:

$ bash -x -c 'set -e; (eval "false && true"; echo hi); echo here'
+ set -e
+ eval 'false && true'
++ false

Basically, the difference is between 'eval'-uating a compound command and just executing a compound command. When the shell executes a compound command, non-terminal commands in the compound command that fail do not cause the entire compound command to fail, they simply terminate the command. But when eval runs the compound command and any non-terminal sub-command terminates the command with an error, eval terminates the command with an error.

I guess I need to format my eval statement like this:

eval "false && true" || :

so that the eval command doesn't exit my subshell with an error, because this works as I'd expect it to:

$ bash -x -c 'set -e; (eval "false && true" || :; echo hi); echo here'
+ set -e
+ false
+ echo hi
+ echo here

The problem I have with this is that I've written a function:

function execute() {
local command="$1"
local remote="$2"
if [ ! -z "$remote" ]; then
$SSH $remote "$command" || :
eval "$command" || :

I'm using
set -e
in my script. The same problem occurs with ssh in this function - if the last command in the ssh script is a compound command that terminates early, the entire command terminates with an error. I want commands like this to behave as if they were executing locally - early terminating compound commands should not cause ssh or eval to return 1, failing the entire command. If I tack
|| :
on the end of my eval statement or my ssh statement, then all such commands will succeed, even if they shouldn't because the last command in the eval'd or ssh'd command failed.

Any ideas would be much appreciated.

Answer Source

I should also mention that set -e is terribly error-prone; see for a bunch of examples. So the best solution might be to dispense with it, and write your own logic to detect errors and abort.

That out of the way . . .

The problem here is that eval "false && true" is a single command, and evaluates to false (nonzero), so set -e aborts after that command runs.

If you were instead to run eval "false && true; true", you would not see this behavior, because then eval evaluates to true (zero). (Note that, although eval does implement the set -e behavior, it obeys the rule that false && true is non-aborting.)

This is not actually specific to eval, by the way. A subshell would give the same result, for the same reason:

$ bash -x -c 'set -e; (false && true); echo here'
+ set -e
+ false

The simplest fix for your problem is probably just to run an extra true if the end is reached:

$SSH $remote "set -e; $command; true"
eval "$command; true"