Bumped into an unexpected bash
/sh
behavior and I wonder someone can explain the rationale behind it, and provide a solution to the question below.
In an interactive bash
shell session, I execute:
$ bash -c 'sleep 10 && echo'
With ps
on Linux it looks like this:
_ -bash
_ bash -c sleep 10 && echo
_ sleep 10
The process tree is what I would expect:
- My interactive bash shell process (
$
) - A children shell process (
bash -c ...
) - a sleep children process
However, if the command portion of my bash -c
is a single command, e.g.:
$ bash -c 'sleep 10'
Then the middle sub-shell is swallowed, and my interactive terminal session executes sleep “directly” as children process. The process tree looks like this:
_ -bash
_ sleep 10
So from process tree perspective, these two produce the same result:
$ bash -c 'sleep 10'
$ sleep 10
What is going on here?
Now to my question: is there a way to force the intermediate shell, regardless of the complexity of the expression passed to bash -c ...
?
(I could append something like ; echo;
to my actual command and that “works”, but I’d rather not. Is there a more proper way to force the intermediate process into existence?)
(edit: typo in ps
output; removed sh
tag as suggested in comments; one more typo)
Advertisement
Answer
There’s actually a comment in the bash source that describes much of the rationale for this feature:
/* If this is a simple command, tell execute_disk_command that it might be able to get away without forking and simply exec. This means things like ( sleep 10 ) will only cause one fork. If we're timing the command or inverting its return value, however, we cannot do this optimization. */ if ((user_subshell || user_coproc) && (tcom->type == cm_simple || tcom->type == cm_subshell) && ((tcom->flags & CMD_TIME_PIPELINE) == 0) && ((tcom->flags & CMD_INVERT_RETURN) == 0)) { tcom->flags |= CMD_NO_FORK; if (tcom->type == cm_simple) tcom->value.Simple->flags |= CMD_NO_FORK; }
In the bash -c '...'
case, the CMD_NO_FORK
flag is set when determined by the should_suppress_fork
function in builtins/evalstring.c
.
It is always to your benefit to let the shell do this. It only happens when:
- Input is from a hardcoded string, and the shell is at the last command in that string.
- There are no further commands, traps, hooks, etc. to be run after the command is complete.
- The exit status does not need to be inverted or otherwise modified.
- No redirections need to be backed out.
This saves memory, causes the startup time of the process to be slightly faster (since it doesn’t need to be fork
ed), and ensures that signals delivered to your PID go direct to the process you’re running, making it possible for the parent of sh -c 'sleep 10'
to determine exactly which signal killed sleep
, should it in fact be killed by a signal.
However, if for some reason you want to inhibit it, you need but set a trap — any trap will do:
# run the noop command (:) at exit bash -c 'trap : EXIT; sleep 10'