4

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.

ruakh
  • 175,680
  • 26
  • 273
  • 307
John Calcote
  • 793
  • 1
  • 8
  • 15
  • 1
    http://mywiki.wooledge.org/BashFAQ/105 has already been recommended; http://fvue.nl/wiki/Bash:_Error_handling is similarly worthwhile reading. (Short form: `-e` has so many corner cases and caveats that its overall value is open to question, and many of the freenode #bash greybeards advise against its use). – Charles Duffy Jun 30 '16 at 15:46
  • Thanks Charles - this was the most helpful response I've seen yet and I can only vote for it - not mark it as the answer because it's only a comment! :) – John Calcote Jun 30 '16 at 16:34

2 Answers2

2

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" 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"
ruakh
  • 175,680
  • 26
  • 273
  • 307
  • The example you give shows that `false` is the last item executed, but in the first example I gave, I showed the same code NOT bailing on the final `echo here` - there was another statement following `false && true`. That's the weird part - if the last statement in the subshell is a *compound* statement that terminates early, the entire subshell returns the value of the failed component of the compound statement, rather than simply not setting an error state, as would be the case if the compound statement were being executed in the root context of the shell. – John Calcote Jun 30 '16 at 00:19
  • @JohnCalcote: Re: your last sentence: Sorry, but you're mistaken about how "the root context of the shell" behaves. Try running `false && true ; echo $?`; you'll see a nonzero value, because of the `false`. (You're probably confusing this with something like `foo | bar`, where what's relevant is the return status of `bar`. But in `foo && bar`, if `foo` returns false then `bar` is never run, so the exit-status of `foo` is what survives.) – ruakh Jun 30 '16 at 00:32
  • I'm sorry, but I'm sitting here watching it happen the way I explained (unless - and I don't discount the possibility - I'm misinterpreting what's happening). Please execute my first example in the original post - you'll see that both `hi` and `here` are echoed. Then remove the `; echo hi` portion of the subshell and run it again - the shell bails (due to `set -e`) right after `false` in the subshell. You never see `here`, as you do with the `echo hi` portion retained in the subshell. – John Calcote Jun 30 '16 at 14:57
  • Incidentally - I reworded the original post, replacing 'pipeline' with 'compound command' - thanks for that. – John Calcote Jun 30 '16 at 15:04
  • Thank you! I see the truth of your solution. :) – John Calcote Jun 30 '16 at 18:10
0

eval counts as its own command with its own exit code.

Since eval "false && true" returns an exit code of 1, it triggers set -e.

that other guy
  • 116,971
  • 11
  • 170
  • 194
  • I understand what you're saying, but normally a statement like `false && true` would not cause `set -e` to bail out of a shell - it's documented that way - when a sub-command fails a compound command early, `set -e` does not cause the shell to terminate. Not so with `eval` and subshells. It appears that a failed sub-command in a compound command actually does cause the compound command to return a non-zero value to the shell, but `set -e` would normally ignore the result of that command - not so with `eval` or subshells. It's just a corner case that never was considered. – John Calcote Jun 30 '16 at 15:27
  • Yes so, with eval and subshells. Any failing command that is not in a conditional statement will cause the shell to exit with `set -e`, and that doesn't change with eval or `(..)`. The exit code of `false` does not cause the shell to exit in either of those cases. It's the exit code of `eval` and `(..)` that does it, since these are commands and not part of conditional statements. – that other guy Jun 30 '16 at 16:16