How does “set -e” work with subshells?

2019-01-26 06:03发布

问题:

I was wondering whether set -e propagates through subshells (i.e. does a subshell inherit the -e setting of its parent), so I made some experiments. I found some strange results that I can't explain.

First, here are some basic tests. They return what I expect.

( true; false )         # 1
( false; true )         # 0
( set -e; false; true ) # 1

Now I tried what happens if I put a subshell within my subshell. This expression returns 1, which suggests that it propagates.

( set -e; ( false; true ) )

Then I tried these expressions. I expected them to return 1, but I found that they return 0.

( set -e; ( true; false ); true )
( set -e; ( set -e; false; true ); true )

Why? In both cases, the inner subshell returns 1, whether set -e propagates or not (as I checked in the beginning). The outer subshell has set -e, which means that it should fail after the inner subshell exits, but it does not. Can someone explain this?

回答1:

Prior to bash 4, set -e appears to only cause the shell to exit if a simple command has a non-zero exit (emphasis mine):

-e Exit immediately if a simple command (see SHELL GRAMMAR above) exits with a non-zero status.

In bash 4 (possibly 4.1, I don't have a 4.0 to check), the effect of -e was extended to more complicated commands:

-e Exit immediately if a pipeline (which may consist of a single simple command), a subshell com- mand enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces (see SHELL GRAMMAR above) exits with a non-zero status.



回答2:

From man bash:

set [+abefhkmnptuvxBCEHPT] [+o option-name] [arg ...]

-e

Exit immediately if a pipeline (which may consist of a single simple command), a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces (see SHELL GRAMMAR above) exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted with !. A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell.

So when a false is found, it exits from the subshell.

See a couple of examples:

# when `false`, exit. Hence, no `echo ho`
$ ( set -e; echo hi; false; echo ho ) ; echo $?
hi
1

# when `false`, exit. Hence, no `echo hu`
$ ( set -e; echo hi; true; echo ho; false; echo hu ) ; echo $?
hi
ho
1


回答3:

As of Bash 4.4, these commands all return 1:

( set -e; ( false; true ) )
( set -e; ( true; false ); true )
( set -e; ( set -e; false; true ); true )

So I think you may be using an older version of Bash which has different implementations about set -e and subshell commands, as pointed out by another answer.