John Calcote John Calcote - 5 months ago 24
Bash Question

Why does eval exit subshell mid-pipe 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
hi
+ echo here
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
hi
+ echo here
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" || :
else
eval "$command" || :
fi
}


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

To start with, let me clarify a piece of terminology, because otherwise I think you'll find it hard to follow and apply the documentation:

A pipeline is a sequence of simple commands separated by one of the control operators ‘|’ or ‘|&’. [link]

So false && true is not a "pipeline"; false and true are both (trivial) pipelines, but in false && true you have two separate pipelines joined with &&.


I should also mention that set -e is terribly error-prone; see http://mywiki.wooledge.org/BashFAQ/105 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" 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"