1

The script below is a minimal (albeit artificial) illustration of the problem.

## demo.sh

exitfn () {
    printf -- 'exitfn PID: %d\n' "$$" >&2
    exit 1
}

printf -- 'script PID: %d\n' "$$" >&2

exitfn | :

printf -- 'SHOULD NEVER SEE THIS (0)\n' >&2

exitfn

printf -- 'SHOULD NEVER SEE THIS (1)\n' >&2

In this example script, exitfn stands for a function whose job entails terminating the current process1.

Unfortunately, as implemented, exitfn does not accomplish this mission reliably.

If one runs this script, the output looks like this:

% bash ./demo.sh
script PID: 26731
exitfn PID: 26731
SHOULD NEVER SEE THIS (0)
exitfn PID: 26731

(Of course, the value shown for PID will be different with each invocation.)

The key point here is that, on the first invocation of the exitfn function, the exit 1 command in its body fails to terminate the execution of the enclosing script (as evidenced by the execution of the first printf command immediately following). In contrast, on the second invocation of exitfn, this exit 1 command does bring the script's execution to an end (as evidenced by the fact that the second printf command is not executed).

The only difference between the two invocations of exitfn is that the first one occurs as the first component of a two-component pipeline, while the second one is a "standalone" invocation.

I am puzzled by this. I had expected that exit would have the effect of killing the current process (i.e. the one with PID given by $$). Obviously, this is not always true.

Be that as it may, is there a way to write exitfn so that it exits the surrounding script even when invoked within a pipeline?


Incidentally, the script above is also a valid zsh script, and produces the same result:

% zsh ./demo.sh
script PID: 26799
exitfn PID: 26799
SHOULD NEVER SEE THIS (0)
exitfn PID: 26799

I'd be interested in the answer to this question for zsh as well.


Finally, I should point out that implementing exitfn like this does not work at all:

exitfn () {
    printf -- 'exitfn PID: %d\n' "$$" >&2
    exit 1
    kill -9 "$$"
}

...because, under all circumstances, the exit 1 command is always the last line of that function that gets executed. (Replacing exit 1 with kill -9 $$ is not acceptable: I want to control the script's exit status, and its output to stderr.)


1In practice, such a function would perform other tasks, such as diagnostics logging, or cleanup operations, before terminating the current process.

3

I had expected that exit would have the effect of killing the current process (i.e. the one with PID given by $$)

“The current process” is not the same thing as “the process with PID given by $$”. exit exits the current subshell, or the original shell if it isn't called in a subshell.

Certain constructs, such as the content of ( … ) (grouping with subshell), command substitutions ($(…) or `…`), and each side of a pipe (or only the left-hand side in some shells), run in a subshell. A subshell behaves as if it was a separate process created with fork(), and it usually is implemented this way (some shells don't use subprocesses in some circumstances, as a performance optimization). The subshell has its own copy of variables, its own redirections, etc. Calling exit exits the subshell, just like the exit() function in the standard C library exits the process. For more about subshells, see What is a subshell (in the context of the documentation of make)?, What is the exact difference between a "subshell" and a "child process"? and Is $() a subshell?.

$$ is always the process ID of the original shell process. It doesn't change in a subshell. Some shells have a variable that changes in a subshell, for example $BASHPID in bash and mksh, or ${.sh.subshell} in ksh or $ZSH_SUBSHELL or $sysparams[pid] in zsh¹.

is there a way to write exitfn so that it exits the surrounding script even when invoked within a pipeline?

Not exactly. You'll need to do more work, either at the point where the script creates subshell, or at the toplevel, or both. See exit shell script from a subshell for approximations.


¹ beware though that contrary to other shells, zsh performs expansions in pipelines in the parent process (from left to right), not in each of the members of the pipelines: zsh -c 'echo $ZSH_SUBSHELL | cat' outputs 0 (even though that echo runs in a child process) and zsh -c 'n=0; echo $((++n)) | echo $((++n))' outputs 2.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.