Bash subshell creation with curly braces
In a pipeline, all commands run concurrently (with their stdout/stdin connected by pipes) so in different processes.
In
cmd1 | cmd2 | cmd3
All three commands run in different processes, so at least two of them have to run in a child process. Some shells run one of them in the current shell process (if builtin like read
or if the pipeline is the last command of the script), but bash
runs them all in their own separate process (except with the lastpipe
option in recent bash
versions and under some specific conditions).
{...}
groups commands. If that group is part of a pipeline, it has to run in a separate process just like a simple command.
In:
{ a; b "$?"; } | c
We need a shell to evaluate that a; b "$?"
is a separate process, so we need a subshell. The shell could optimise by not forking for b
since it's the last command to be run in that group. Some shells do it, but apparently not bash
.
Nesting the curly braces would seem to denote that you're creating an additional level of scoping which requires a new sub-shell to be invoked. You can see this effect with the 2nd copy of Bash in your ps -H
output.
Only the processes stipulated in the first level of curly braces are run within the scope of the original Bash shell. Any nested curly braces will run in their own scoped Bash shell.
Example
$ { { { sleep 20; } | sleep 20; } | ps -H; }
PID TTY TIME CMD
29190 pts/1 00:00:00 bash
5012 pts/1 00:00:00 bash
5014 pts/1 00:00:00 bash
5016 pts/1 00:00:00 sleep
5015 pts/1 00:00:00 sleep
5013 pts/1 00:00:00 ps
Taking the | ps -H
out of the mix just so we can see the nested curly braces, we can run ps auxf | less
in another shell.
saml 29190 0.0 0.0 117056 3004 pts/1 Ss 13:39 0:00 \_ bash
saml 5191 0.0 0.0 117056 2336 pts/1 S+ 14:42 0:00 | \_ bash
saml 5193 0.0 0.0 107892 512 pts/1 S+ 14:42 0:00 | | \_ sleep 20
saml 5192 0.0 0.0 107892 508 pts/1 S+ 14:42 0:00 | \_ sleep 20
saml 5068 0.2 0.0 116824 3416 pts/6 Ss 14:42 0:00 \_ bash
saml 5195 0.0 0.0 115020 1272 pts/6 R+ 14:42 0:00 \_ ps auxf
saml 5196 0.0 0.0 110244 880 pts/6 S+ 14:42 0:00 \_ less
But wait there's more!
If you take out the pipes though and use this form of a command we see what you'd actually expect:
$ { { { sleep 10; } ; { sleep 10; } ; sleep 10; } } | watch "ps -H"
Now in the resulting watch window we get a update every 2 seconds of what's going on:
Here's the first sleep 10
:
PID TTY TIME CMD
29190 pts/1 00:00:00 bash
5676 pts/1 00:00:00 bash
5678 pts/1 00:00:00 sleep
5677 pts/1 00:00:00 watch
5681 pts/1 00:00:00 watch
5682 pts/1 00:00:00 ps
Here's the second sleep 10
:
PID TTY TIME CMD
29190 pts/1 00:00:00 bash
5676 pts/1 00:00:00 bash
5691 pts/1 00:00:00 sleep
5677 pts/1 00:00:00 watch
5694 pts/1 00:00:00 watch
5695 pts/1 00:00:00 ps
Here's the third sleep 10
:
PID TTY TIME CMD
29190 pts/1 00:00:00 bash
5676 pts/1 00:00:00 bash
5704 pts/1 00:00:00 sleep
5677 pts/1 00:00:00 watch
5710 pts/1 00:00:00 watch
5711 pts/1 00:00:00 ps
Notice all three sleeps though invoked at different nesting levels of curly braces do infact stay within the PID 5676 of Bash. So I believe your issue is self inflicted with the use of | ps -H
.
Conclusions
The use of | ps -H
(i.e. the pipe) is causing an additional sub-shell, so don't use that method when attempting to interrogate what's going on.
I'll post results of my tests, which leads me to conclusion that bash makes a sub-shell for a group command if and only if it's a part of pipeline, that is similar as if one would call some function which would be also called in sub-shell.
$ { A=1; { A=2; sleep 2; } ; echo $A; }
2
$ { A=1; { A=2; sleep 2; } | sleep 1; echo $A; }
1