41

According with:

3.2.5.3 Grouping Commands

{}

{ list; }

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?

2
  • 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? Apr 30, 2014 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; } Apr 30, 2014 at 18:37

3 Answers 3

32

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.

6
  • "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, 2017 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, 2017 at 7:44
  • @HakanBaba, I've changed that to "child process" to avoid potential confusion. Oct 28, 2017 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. Oct 28, 2017 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, 2017 at 7:55
21

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.

5
  • 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, 2014 at 19:25
  • @xealits - is that a follow up Q you're asking me?
    – slm
    Nov 30, 2014 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, 2014 at 17:12
  • now, after rereading your post, I see I was wrong -- the nesting statement concerns only the case with pipelines. So, the curly braces don't ever create new shells, unless you wrap a separate process with them -- then they have to.
    – xealits
    Dec 1, 2014 at 17:15
  • @xealits - that's correct.
    – slm
    Dec 1, 2014 at 17:15
8

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

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .