According to this, placing a list of commands between curly braces causes the list to be executed in the current shell context. No subshell is created.

Using ps to see this in action

This is the process hierarchy for a process pipeline executed directly on command line. 4398 is the PID for the login shell:

sleep 2 | ps -H;
  PID TTY          TIME CMD
   4398 pts/23   00:00:00 bash
   29696 pts/23   00:00:00   sleep
   29697 pts/23   00:00:00   ps

Now follows the process hierarchy for a process pipeline between curly braces executed directly on command line. 4398 is the PID for the login shell. It's similar to the hierarchy above proving that everything is executed in current shell context:

{ sleep 2 | ps -H; }
   PID TTY          TIME CMD
    4398 pts/23   00:00:00 bash
    29588 pts/23   00:00:00   sleep
    29589 pts/23   00:00:00   ps

Now, this is the process hierarchy when the sleep in the pipeline is itself placed inside curly braces (so two levels of braces in all)

{ { sleep 2; } | ps -H; }
  PID TTY          TIME CMD
   4398 pts/23   00:00:00 bash
   29869 pts/23   00:00:00   bash
   29871 pts/23   00:00:00     sleep
   29870 pts/23   00:00:00   ps

Why does bash have to create a subshell to run sleep in the 3rd case when the documentation states that commands between curly braces are executed in current shell context?

  • Interesting, I would guess it's because in the 3rd case the inner group is part of pipeline, and hence it's executed in sub-shell e.g., as any other function call which is part of pipeline would be. Does it make sense? – Miroslav Koškár Apr 30 '14 at 18:30
  • 2
    I would not say "The shell must" just because it does... Pipelines are not executed in the shell context. If the pipeline consists of nothing but external commands then creating subprocesses is enough. { sleep 2 | command ps -H; } – Hauke Laging Apr 30 '14 at 18:37
up vote 25 down vote accepted

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.

  • "All three commands run in different processes, so at least two of them have to run in a subshell." Why is a subshell necessary in this case ? Can the parent shell not spawn tree processes ? Or, When you say "have to run in a subshell", do you mean the shell will fork itself, and then exec for each cmd1 cmd2 and cmd3 ? If I execute this bash -c "sleep 112345 | cat | cat " I only see one bash created and then 3 children to it without any other interleaving sub bashes. – Hakan Baba Oct 28 '17 at 7:40
  • "We need a shell to evaluate that a; b "$?" is a separate process, so we need a subshell." Could you also expand the reasoning ? Why we need a subsheel to understand that? What does it take to understand that? I presume parsing is required but what else ? . Can the parent shell not parse a; b "$?" ? Is there really a fundamental need for a subsheel, or maybe is it a design decision/ implementation on bash? – Hakan Baba Oct 28 '17 at 7:44
  • @HakanBaba, I've changed that to "child process" to avoid potential confusion. – Stéphane Chazelas Oct 28 '17 at 7:44
  • 1
    @HakanBaba, the parsing is done in the parent (the process that reads the code, the one that executed the interpreter unless that code was passed to eval), but the evaluation (run the first command, wait for it, run the second) is done in the child, the one that has stdout connected to the pipe. – Stéphane Chazelas Oct 28 '17 at 7:48
  • in { sleep 2 | ps -H; } the parent bash sees sleep 2 that requires a fork/exec. But in { { sleep 2; } | ps -H; } the parent bash sees { sleep 2; } in other words, some bash code. It looks like the parent can handle the fork/exec for sleep 2 but spawns a new bash recursively to handle the encountered bash code. That is my understanding, does it make sense ? – Hakan Baba Oct 28 '17 at 7:55

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.

  • slm, thank you for the great tips! – iruvar May 1 '14 at 13:03
  • so, "Only the processes stipulated in the first level of curly braces are run within the scope of the original Bash shell." – xealits Nov 30 '14 at 19:25
  • @xealits - is that a follow up Q you're asking me? – slm Nov 30 '14 at 22:36
  • @slm it's just the emphasis on the main point of the answer, as I saw it. Commands in the first level of curly braces run in current shell, nested curly braces create new shells. Parentheses differ in creating subshell right away, at the first level. If I got it wrong -- correct me. But, as others point, the initial question has also pipelines. Thus the creation of separate processes. And curly braces have to create a shell when used for a separate process. So, probably, it is the reason for the behaviour in question. – xealits Dec 1 '14 at 17:12
  • @xealits - Ah, OK thanks for replying. – slm Dec 1 '14 at 17:14

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
  • My A is showing this as well. – slm Apr 30 '14 at 18:57

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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