Piping 'false' does not give non-zero result code

The result of both true | echo "$?" and false | echo "$?" is misleading. The content of "$?" will be set before piping the command false to the command echo.

To execute these lines, bash sets up a pipeline of commands. The pipeline is setup, then the commands started in parallel. So in your example:

true;   echo "$?"      # 0
false;  echo "$?"      # 1
true  | echo "$?"      # 0

is the same as:

true   
echo "$?"      # 0
false  
echo "$?"      # 1
echo "$?"      # 0

true is not executed before echo $? it is executed at the same time.


In false | echo $?, $? is not the exit status of false because $? expands to the decimal exit status of the most recent pipeline [1], not that of the most recent command, subshell or child process. In false | echo $?, false is not a pipeline, but just part of one. A simple complete command like false; is a pipeline, even if does not contain any |.

Assuming that set -o pipefail is on and the exit status of false | echo $? will be that of false, $? could not be exit the status of the current pipeline either, since the current pipeline hasn't yet exited by the time it echoes it.

It does not matter the order in which the two sides of the pipeline (which are always run in parallel) are started or terminated, or whether the echo $? is actually run in a child process or subshell.

FWIW, when in a subshell, variables and other parameters are always expanded in the context of the current subshell: if false | echo $? is implemented by forking separate processes for both sides of the pipeline (which is the case in bash, but not in all shells), $? will be expanded in the child process, after the fork(), and possibly after the left side of the pipeline has exited [2].

And how could I force a failure in a pipe, and get 1 thereafter?

You use set -o pipefail, which is supported in bash, zsh and ksh and should be included in a future version of the standard. But as explained above, this only affects the value of $? after the pipeline has exited, not within the pipeline itself.


[1] 2.5.2 Special Parameters in SUSv4 standard. Whether a command substitution is considered a pipeline in this context depends on the shell; in most historical and present shells :; echo `exit 13` $? will print 13, but in some (like dash, yash or pdksh-derived) it will print 0.

[2] A simple bash example which may help to understand that is echo $BASHPID >&2 | echo $BASHPID >&2 | echo $BASHPID >&2; the BASHPID variable is not expanded before the 3 child processes are set up and run. Special parameters like $? are not special in this regard; they're expanded like any other variables.


This is because both parts of the pipe are run in parallel, so the false command has not yet completed (and set $?) when the echo command already starts to print $?, which is still the result of the previous command; in your case most likely the last echo executed (and that was probably successful).