Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 11 · Exit Status and Command Chaining

Grouping, Subshells and What Survives

Two ways to treat several commands as one unit, differing in a single respect that changes everything downstream: one of them starts a new shell and the other does not. That difference decides whether a directory change, a variable, an option or an exit survives past the closing bracket.

Intermediate18 min readLinuxPart IFundamentalsExit StatusBash

Part I · Module 11 · Chapter 11.4 · Grouping, Subshells and What Survives

Chapter 11.3 used || { echo "..."; exit 1; } as the standard guard and ( … ) in half its Practice blocks, promising an explanation. Several earlier chapters did the same. This is it.

1. One Unit, Two Ways

Both of these treat a set of commands as a single thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( cd build; make )
{ cd build; make; }

They look interchangeable. They differ in exactly one respect, and it is the one that matters:

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cd /tmp/demo
$ ( cd build; echo "  inside ( ): $(pwd)" ); echo "  after   ( ): $(pwd)"
  inside ( ): /tmp/demo/build
  after   ( ): /tmp/demo

$ cd /tmp/demo
$ { cd build; echo "  inside { }: $(pwd)"; }; echo "  after   { }: $(pwd)"
  inside { }: /tmp/demo/build
  after   { }: /tmp/demo/build

Same two commands. After the parentheses you are back where you started; after the braces you have moved.

The difference between round brackets and curly braces. Round brackets start a separate shell, which works on a copy of everything and discards it, so a directory change inside them does not reach you. Curly braces group commands in the shell you are already in, so every change they make is yours.Round bracketsa separate shell is startedIt works on a copyof the directory, thevariables, the optionsNothing escapes itwhich is what makes it a safesandboxCurly bracesno new shell at allThe lines run in yoursgrouped only so they share oneredirectEvery change staysincluding the ones you did notintend12
Figure 1 — the difference between round brackets and curly braces. Round brackets start a separate shell, which works on a copy of the directory, the variables and the options, and discards that copy when it finishes — so nothing inside reaches you. Curly braces group commands in the shell you are already in, so every change they make is yours, including the ones you did not intend.

2. The Brace Syntax Is Fussy

Parentheses behave like punctuation anywhere. Braces are reserved words, which means the shell has to recognise them as separate words — and that imposes two requirements people trip over constantly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ { echo ok; }
ok

$ {echo bad; }
bash: syntax error near unexpected token `}'

$ { echo bad }
bash: syntax error: unexpected end of file

You need a space after {, because {echo is read as one word. And you need a ; or a newline before }, because otherwise } is read as an argument to echo and the group is never closed — which is why the second error complains about the end of the file rather than about the brace.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{ echo a; echo b; }           # on one line: space after {, ; before }
{
  echo a
  echo b
}                             # across lines: the newline does the job

Parentheses need neither:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ (echo ok)
ok

3. What a Subshell Contains

A subshell is a child process, so Chapter 7.4's inheritance rules apply exactly. It receives a copy of everything and returns nothing but its exit status.

Variables:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ v=outer
$ ( v=inner; echo "  inside: $v" ); echo "  after:  $v"
  inside: inner
  after:  outer

Shell options — which is why Chapter 9.6 suggested experimenting inside parentheses:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ shopt -u nullglob
$ ( shopt -s nullglob; shopt nullglob )
nullglob       	on
$ shopt nullglob
nullglob       	off

The working directory, as §1 showed. And the exit status does come back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ( exit 5 ); echo "  after ( ): still alive, status $?"
  after ( ): still alive, status 5

exit inside a subshell ends the subshell, not your shell. Inside braces it ends everything:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ bash -c '{ echo "  in group"; exit 5; }; echo "  NOT REACHED"'
  in group
$ echo "  that shell exited with $?"
  that shell exited with 5

That difference is worth holding onto: ( exit 1 ) is harmless and { exit 1; } is not, which makes parentheses the safe way to try something that might end badly.

4. Redirecting a Whole Group

This is what braces are genuinely for, and it is the most common legitimate use of either construct.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ { echo a; echo b; } > out.txt
$ cat out.txt
a
b

One redirect, applied to everything in the group. Without it you would write > on every line and the second would have to be >>:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo a >  out.txt
echo b >> out.txt

Which is noisier, and wrong the moment somebody reorders the lines.

Parentheses redirect the same way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ( echo c; echo d ) > out2.txt

Prefer braces for this. There is no reason to start a process just to redirect, and a subshell adds a fork for nothing.

The pattern turns up most in building a report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{
  echo "generated : $(date +%F' '%T)"
  echo "host      : $(hostname -s)"
  echo "run       : $run_name"
  echo
  echo "passed    : $pass"
  echo "failed    : $fail"
} > "$report"

Eight lines, one redirect, and the whole block can be moved or wrapped in a conditional without touching the redirection. (The > operator itself is Module 12.)

5. When to Use Which

The cd sandbox

The single most useful application, and the proper answer to Chapter 11.2's hazard:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( cd "$build_dir" && make ) || { echo "build failed" >&2; exit 1; }

The cd applies only inside the parentheses. The rest of the script continues in the directory it was already in, and you do not have to remember to cd back — which matters, because a cd - after a command that failed will not run.

Compare the three approaches:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cd build; make; cd ..                    # breaks if cd or make fails
cd build && make; cd ..                  # better, but the cd .. still fires blindly
( cd build && make )                     # nothing to undo

The third has no cleanup step at all, because there is nothing to clean up.

Grouping for a conditional

Chapter 11.3 used this shape repeatedly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cd "$d" || { echo "cannot enter $d" >&2; exit 1; }

Braces, not parentheses — deliberately. exit must end the script, and inside parentheses it would only end the subshell, leaving the script running with its precondition unmet. Getting that backwards produces a guard that prints its message and then carries on regardless.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cd "$d" || ( echo "cannot enter $d" >&2; exit 1 )     # BROKEN — the script continues
cd "$d" || { echo "cannot enter $d" >&2; exit 1; }    # correct

Running a block in parallel

Because a subshell is a process, it can be backgrounded (Chapter 7.5):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( cd run_1 && ./simulate.sh ) > run_1.log 2>&1 &
( cd run_2 && ./simulate.sh ) > run_2.log 2>&1 &
( cd run_3 && ./simulate.sh ) > run_3.log 2>&1 &
wait
echo "all three finished"

Three simulations at once, each in its own directory, with no interference — and the parent shell's directory untouched throughout. Braces could not do this; backgrounding a group is not a thing.

6. The Subshells You Did Not Write

Parentheses are the explicit form. Several constructs start a subshell without one, and recognising them explains behaviour from four earlier chapters.

ConstructWhy there is a subshell
$( … )Command substitution — Chapter 8.4
`…`The older spelling, same thing
cmd1 | cmd2Every stage of a pipeline
( … ) &A backgrounded group
cmd &Any backgrounded command

The pipeline one is the expensive surprise, and Chapter 8.6 measured it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ n=0; cat data.txt | while read -r l; do n=$(( n + 1 )); done; echo "after pipe:     n=[$n]"
after pipe:     n=[0]

$ n=0; while read -r l; do n=$(( n + 1 )); done < data.txt;  echo "after redirect: n=[$n]"
after redirect: n=[3]

The while loop was the right-hand side of a pipe, so it ran in a subshell, incremented its copy of n, and discarded it. Redirecting instead of piping removes the subshell and the counter survives.

And Chapter 8.4's script-directory idiom is a subshell used on purpose:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
script_dir=$(cd "$(dirname "$0")" && pwd)

The cd happens inside the substitution's subshell, so your working directory is untouched while the absolute path still comes back. That is the whole reason the idiom is safe.

7. Putting It Together

A regression runner using every construct from the module:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
set -uo pipefail

runs_root="${1:?usage: $0 <runs-root>}"
report="$runs_root/summary.txt"

[ -d "$runs_root" ] || { echo "no such directory: $runs_root" >&2; exit 2; }

pass=0
fail=0

# A subshell per run: each gets its own directory, none affects the others
# or us, and all three can be in flight at once.
for d in "$runs_root"/run_*; do
  ( cd "$d" && ./simulate.sh ) > "$d/sim.log" 2>&1 &
done
wait

# No subshell here — the counters must survive, so this reads from a glob
# rather than a pipe.
for d in "$runs_root"/run_*; do
  if grep -q 'TEST PASSED' "$d/sim.log" 2>/dev/null; then
    pass=$(( pass + 1 ))
  else
    fail=$(( fail + 1 ))
    echo "FAILED: ${d##*/}"
  fi
done

# Braces, so one redirect covers the whole report.
{
  echo "generated : $(date +%F' '%T)"
  echo "root      : $runs_root"
  echo "passed    : $pass"
  echo "failed    : $fail"
} > "$report"

echo "wrote $report"
[ "$fail" -eq 0 ] || exit 1

Five decisions worth naming:

  • ( cd "$d" && … ) & — parentheses because each run needs its own directory and none of them should change the script's; backgrounded because a subshell is a process.
  • || { …; exit 2; } — braces, because exit must end the script.
  • The counting loop reads a glob, not a pipe, so pass and fail survive.
  • { … } > "$report" — braces for the redirect, no process started.
  • The final exit 1 gives the script the meaningful status Chapter 11.1 argued for.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/grp/build && cd /tmp/grp

1 — See the directory difference.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cd /tmp/grp; ( cd build; echo "  inside ( ): $(pwd)" ); echo "  after ( ): $(pwd)"
cd /tmp/grp; { cd build; echo "  inside { }: $(pwd)"; }; echo "  after { }: $(pwd)"
cd /tmp/grp

2 — Meet both syntax errors.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{ echo ok; }
{echo bad; }
{ echo bad }

Press Ctrl-C if the third leaves you at a continuation prompt.

3 — Scope a variable and an option.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
v=outer; ( v=inner; echo "  inside: $v" ); echo "  after: $v"
shopt -u nullglob
( shopt -s nullglob; shopt nullglob )
shopt nullglob

4 — Compare the two exits.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( echo "  in subshell"; exit 5 ); echo "  shell alive, status $?"
bash -c '{ echo "  in group"; exit 5; }; echo "  NOT REACHED"'; echo "  that shell: $?"

5 — Redirect a group.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{ echo a; echo b; } > out.txt; cat out.txt
( echo c; echo d ) > out2.txt; cat out2.txt

6 — Build the cd sandbox.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cd /tmp/grp
( cd build && pwd ) && echo "  back in $(pwd)"
( cd nowhere && pwd ) || echo "  failed inside, and we are still in $(pwd)"

7 — Catch the broken guard.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( cd /nonexistent || ( echo "  subshell guard"; exit 1 ); echo "  STILL RAN — the guard did not stop it" )
( cd /nonexistent || { echo "  brace guard"; exit 1; }; echo "  NOT REACHED" )

8 — Lose a counter to a pipe, then rescue it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'a\nb\nc\n' > three.txt
n=0; cat three.txt | while read -r l; do n=$(( n + 1 )); done; echo "  piped:    $n"
n=0; while read -r l; do n=$(( n + 1 )); done < three.txt;     echo "  redirect: $n"

9 — Background three subshells.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for i in 1 2 3; do ( sleep "$i"; echo "  run $i done" ) & done
wait
echo "  all finished"

10 — Clean up. cd /tmp && rm -rf /tmp/grp

Check Your Understanding

1. What is the one difference between ( … ) and { … }, and what follows from it?

2. Why does { echo a; echo b } fail?

3. ( cd build && make ) — where are you afterwards, and why is that useful?

4. cd "$d" || ( echo "failed" >&2; exit 1 ) prints the message and the script carries on. Why, and what is the fix?

5. A while loop counts lines and the total is zero afterwards. What is the likely cause?

6. You want eight lines written to one file. Which construct, and why not the other?

8. Remember

  • ( … ) starts a subshell; { … } groups in the current shell. Everything else follows.
  • Nothing escapes a subshell — not a directory change, a variable, a shell option, or anything else. Only the exit status comes back.
  • Everything escapes a brace group, because there was never a boundary.
  • Braces are fussy: a space after {, and a ; or newline before }. They are reserved words, not punctuation.
  • ( exit 1 ) is harmless; { exit 1; } ends your shell.
  • Use parentheses when you want the changes thrown away, braces for everything else.
  • ( cd "$dir" && make ) is the clean directory sandbox — no cd - to remember and nothing to undo if it fails.
  • A guard that must stop the script needs braces. || ( …; exit 1 ) prints its message and carries on.
  • { …; } > file is the right way to redirect a block. Parentheses fork for nothing.
  • A subshell is a process, so it can be backgrounded — which is how you run several directory-scoped jobs at once.
  • Several constructs make a subshell without parentheses: $( … ), every stage of a pipeline, and anything backgrounded.
  • A counter that is zero after a loop means a pipe. Redirect, or use process substitution — Chapter 17.7 measures this one, including the exit that does not exit.
  • $$ does not change inside a subshell, and $BASHPID — which does — needs Bash 4.

That completes Module 11. You can now read the number every command returns, chain commands so that a failure stops the sequence rather than being ignored, recognise the one chaining shape that looks like a conditional and is not, and group commands with a deliberate choice about whether their effects survive.

Module 12 takes the last structural piece of the shell: the three streams every process is born with, the operators that redirect them to and from files, and the pipe — the construct that turns a handful of small tools into a toolkit, and the one that has been quietly creating subshells behind your back since Chapter 8.6.

Where this fits

Part of the Linux & Bash curriculum.