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

Tags:

Bash

Subshell