Bash Subshells: () vs {}, Variable Scope and Examples

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
Applies to Linux distributions with Bash installed
Privilege Normal user; examples use /tmp/bash-subshell-lab
Scope Bash execution environments: ( list ) subshell groups, { list; } current-shell groups, group redirection, pipeline, command-substitution and process-substitution scope, lastpipe, BASH_SUBSHELL, BASHPID, executing vs sourcing scripts, and external processes vs Bash subshell rules. Does not cover full trap inheritance or set -e in grouped commands.
Related guides Bash pipes
Shell scripting course hub
Short on time?Beta Get the gist of this 13-minute read in seconds, then ask follow-up questions

A Bash subshell runs commands in an isolated shell environment. Changes such as cd, variable assignments, and shell options made inside the subshell normally disappear when it finishes.

The most common way to create one is with parentheses:

bash
( commands )

Curly braces look similar, but { commands; } run in the current shell instead. That difference explains many Bash problems where a variable or directory change unexpectedly disappears.


What Bash Means by a Subshell

A Bash subshell is a separate execution environment for a list of shell commands. Commands inside it can change variables, the working directory, or shell options, but those changes normally disappear when the subshell ends.

Two grouping forms look similar but behave differently:

  • ( list ) runs list in a subshell environment.
  • { list; } runs list in the current shell.

Both groups return the exit status of their command list. The difference is whether assignments and cd survive after the closing ) or }.


Parentheses ( ): Isolated Group

Parentheses are shell operators. Bash runs the commands inside in a subshell, so the parent shell keeps its own variables and working directory.

Save your starting directory, then run cd inside parentheses:

bash
mkdir -p /tmp/bash-subshell-lab
cd /tmp/bash-subshell-lab
(cd /tmp; pwd)
pwd
output
/tmp
/tmp/bash-subshell-lab

The subshell printed /tmp, but the parent shell's working directory stayed in /tmp/bash-subshell-lab.

The same isolation applies to shell variables:

bash
x=1
(x=2; echo "inside x=$x")
echo "parent x=$x"
output
inside x=2
parent x=1

x=2 existed only inside the subshell. The parent x remained 1.

Check the Exit Status of a Subshell

A parenthesized group returns the exit status of its command list. Capture it with $? immediately after the subshell finishes:

bash
mkdir -p /tmp/bash-subshell-lab/logs
printf '2026-09-10 ERROR fail\n' > /tmp/bash-subshell-lab/logs/app.log
( grep ERROR /tmp/bash-subshell-lab/logs/app.log )
status=$?
printf 'status=%s\n' "$status"
output
2026-09-10 ERROR fail
status=0

When the subshell command fails, the parent shell receives that non-zero status:

bash
( cd /does-not-exist 2>/dev/null )
printf 'status=%s\n' "$?"
output
status=1

The parent shell receives the exit status of the subshell's command list, so ( … ) can be used directly in if, &&, and || conditions.


Braces { }: Current-Shell Group

Braces form a reserved-word command group. Bash runs the list in the current shell, so variable assignments and cd persist after the group.

Brace syntax is stricter because { and } are reserved words rather than operators. Separate { from the command list, and end the list with ; or a newline before }.

Valid shape:

bash
{ command1; command2; }

Run the same cd test with braces:

bash
cd /tmp/bash-subshell-lab
{ cd /tmp; pwd; }
pwd
output
/tmp
/tmp

After the brace group, the parent shell's working directory is /tmp. If several commands need to be grouped while keeping the directory change in the current shell, a brace group such as { cd /tmp; some-command; } can be used.

Variable assignments persist the same way:

bash
y=1
{ y=2; echo "inside y=$y"; }
echo "parent y=$y"
output
inside y=2
parent y=2

() vs {} — Decision Table

Need Use Why
Isolate temporary shell-state changes ( list ) Variable, option, and directory changes disappear when the subshell exits
Apply several commands in the current shell { list; } Assignments and cd remain visible afterward
Redirect stdout for multiple commands at once Either ( … ) > file or { …; } > file Redirection attaches to the whole group
Keep brace group syntax valid { cmd1; cmd2; } Braces require ; or newline before }

When you need to group several commands for one redirection while isolating their side effects, parentheses are useful. When the block must update caller state, braces or a plain sequence without grouping is the right tool.

( ) vs $( ) in Bash

Parentheses group commands in a subshell while leaving their stdout on the normal output stream. Command substitution also uses a subshell, but captures its stdout and substitutes that text into the surrounding command.

Compare grouping with capture from the lab directory:

bash
cd /tmp/bash-subshell-lab
( cd /tmp; pwd )
dir=$(cd /tmp; pwd)
printf 'dir=%s\n' "$dir"
output
/tmp
dir=/tmp

( … ) printed /tmp to stdout. $( … ) ran the same commands in a subshell environment but assigned the captured path to dir. Neither cd changed the parent working directory.

Syntax Purpose Output
( commands ) Run a grouped list in a subshell Goes to normal stdout
$(commands) Run commands and substitute their stdout Captured into surrounding command or expression
{ commands; } Group commands in the current shell Goes to normal stdout

Variable and Directory Scope

Parentheses are especially useful when several commands need a temporary working directory because you do not need to save and restore $PWD.

Run a short search from a subdirectory, then confirm the parent directory is unchanged:

bash
mkdir -p /tmp/bash-subshell-lab/logs
printf '2026-09-10 ERROR fail\n' > /tmp/bash-subshell-lab/logs/app.log
cd /tmp/bash-subshell-lab
(
    cd logs || exit
    grep ERROR app.log
)
pwd
output
2026-09-10 ERROR fail
/tmp/bash-subshell-lab

grep ran inside the subshell after cd logs. pwd still shows /tmp/bash-subshell-lab because that directory change did not survive.

Brace groups fit when the script genuinely needs the new directory for everything that follows. Put the remaining logic inside the same { …; } block, or accept that the whole script's cwd changed.


Redirect an Entire Command Group

Redirection operators can apply to a whole group instead of a single command. Both grouping forms accept one redirect for every command in the list. See Bash redirection for descriptor details; here the point is group scope.

Send two echo lines to one file from a parenthesized group:

bash
cd /tmp/bash-subshell-lab
rm -f group.log
(echo line1; echo line2) > group.log
cat group.log
output
line1
line2

The same pattern works with braces:

bash
rm -f group2.log
{ echo brace1; echo brace2; } > group2.log
cat group2.log
output
brace1
brace2

Both redirects captured stdout from every command in the group.


Where Bash Creates Subshell Environments Automatically

You do not need parentheses to enter a subshell. Bash also creates subshell environments for several other constructs:

  • Standard command substitution — $( list ) and legacy backticks run the command list in a subshell environment. Bash 5.3 also has current-shell command-substitution forms; see the dedicated command-substitution guide.
  • Pipeline elements — Bash normally executes each command in a multi-command pipeline in a separate process and subshell environment. With lastpipe, the final command can run in the current shell when job control is disabled.
  • Asynchronous commands — commands run with & execute asynchronously in a subshell environment.
  • Process substitution — <( list ) and >( list ) run list asynchronously in a separate process; changes made inside that command do not update the parent shell. Process substitution is a Bash feature and is not part of basic POSIX sh.
  • Explicit ( list ) — the grouping operator from the sections above.

Command substitution is a common surprise. Assignments inside the substitution do not reach the parent shell:

bash
z=1
out=$(z=2; echo "z=$z")
echo "out=$out parent z=$z"
output
out=z=2 parent z=1

The capture shows z=2 from inside the subshell, but the parent's z stayed 1. Bash command substitution covers quoting, nesting, exit status, and Bash 5.3 current-shell forms for $(…).

Background execution also isolates shell-state changes. A brace group normally runs in the current shell, but & makes the asynchronous group execute in a subshell environment:

bash
x=0
{ sleep 0.1; x=1; } &
wait
echo "async parent x=$x"
output
async parent x=0

The background group could set x=1 internally, but its assignment could not change the parent's x.


The Pipeline Variable-Loss Problem

Pipelines are the other everyday subshell trap. Bash normally executes each command in a multi-command pipeline in a separate process and subshell environment, so a while read loop on the right side of a pipe cannot update a counter in the parent shell.

bash
count=0
printf 'a\nb\nc\n' | while read -r line; do count=$((count + 1)); done
echo "count=$count"
output
count=0

The loop incremented count inside the pipeline's subshell. The parent variable never changed. Read a file line by line in Bash shows process-substitution patterns that keep the loop in the current shell.


lastpipe — Advanced Exception

With shopt -s lastpipe and job control disabled, Bash runs the last pipeline command in the current shell instead of its normal subshell environment. That is the main exception to the pipeline rule above.

bash
bash -c 'shopt -s lastpipe; count=0; printf "a\nb\n" | while read -r line; do count=$((count+1)); done; echo count=$count'
output
count=2

lastpipe works only when the option is enabled and job control is not active. For Bash scripts, process substitution such as while read …; done < <(command) is often clearer when the loop must modify variables in the current shell.


External Processes vs Bash Subshells

An external command such as grep normally runs as a child process, but that does not make it a Bash subshell. Here, "subshell" means Bash is executing shell code in an isolated copy of the shell environment.

This distinction matters most for shell variables, builtins, functions, options, and cd. Changes to that shell state cannot flow back from a subshell to its parent.

Running a Script vs Sourcing It

Running a script normally executes it in a separate process. bash script.sh specifically starts another Bash process, while sourcing runs the file's commands in the current shell.

Create a small script in the lab directory:

bash
cat > /tmp/bash-subshell-lab/change.sh <<'EOF'
cd /tmp
value=changed
EOF

Run it with a separate Bash process:

bash
cd /tmp/bash-subshell-lab
value=before
bash /tmp/bash-subshell-lab/change.sh
printf 'after bash: value=%s pwd=%s\n' "$value" "$PWD"
output
after bash: value=before pwd=/tmp/bash-subshell-lab

The script changed its own process, not the caller's shell.

Source the same file in the current shell:

bash
cd /tmp/bash-subshell-lab
value=before
source /tmp/bash-subshell-lab/change.sh
printf 'after source: value=%s pwd=%s\n' "$value" "$PWD"
output
after source: value=changed pwd=/tmp

cd and value persisted because source ran the commands here. See Bash source vs execute a script for when to use each approach.

Method Where commands run Changes affect current shell?
bash script.sh New Bash process No
./script.sh (executable script with shebang) Separate process via its interpreter No
source script.sh / . script.sh Current shell Yes

Inspect Nesting with BASH_SUBSHELL

BASH_SUBSHELL is a special Bash variable that indicates the current subshell nesting level. At the top level it is normally 0, and it increases as Bash enters nested subshell environments. It is useful when you want to confirm whether Bash is currently executing inside a nested subshell.

bash
echo "top=$BASH_SUBSHELL"
(echo "parens=$BASH_SUBSHELL"; (echo "nested=$BASH_SUBSHELL"))
{ echo "braces=$BASH_SUBSHELL"; }
output
top=0
parens=1
nested=2
braces=0

Parentheses increased the nesting level. The brace group stayed at 0 because it ran in the current shell.

If you want to inspect the process ID as well, use BASHPID. $$ normally continues to expand to the PID of the original shell inside ( … ), while BASHPID reflects the current Bash process:

bash
printf 'parent: $$=%s BASHPID=%s\n' "$$" "$BASHPID"
(
    printf 'subshell: $$=%s BASHPID=%s\n' "$$" "$BASHPID"
)
output
parent: $$=143476 BASHPID=143476
subshell: $$=143476 BASHPID=143490

$$ still identifies the original shell, while BASHPID shows the Bash process executing the subshell.


Practical Uses for Intentional Isolation

Use ( … ) when a block needs temporary shell-state changes that should not affect the parent shell:

  • try a different working directory without moving the script
  • set several temporary variables or shell options for a block of commands (for example ( set +x; commands ))

Use { …; } when several commands need to be grouped without isolating their changes:

  • run several builtins or assignments and leave the results in the caller's scope
  • share one redirection or here-document across multiple commands

Functions called normally run in the current shell. Running a function in a pipeline, command substitution, asynchronous context, or explicit ( … ) subshell can instead isolate its changes from the caller. See Bash functions for how local variables and dynamic scope interact with these groups.


Common Misconceptions

Misconception Reality
{ } is a subshell Braces run in the current shell; only ( ) creates a subshell group
Every command starts a subshell External programs are child processes; Bash subshell rules apply to shell lists and specific constructs
cd inside () changes the script directory Parent directory is unchanged after the subshell exits
Pipelines always share variables Each pipeline command normally runs in a separate process and subshell environment unless lastpipe applies to the final stage
$() is "just text replacement" Command substitution runs commands in a subshell; side effects do not persist
echo $$ proves no subshell formed $$ often stays the parent PID inside ( … ); compare BASHPID instead
Running script.sh can change my current shell A separately executed script cannot change its caller's variables or cwd; source script.sh runs commands in the current shell

Both ( list ) and { list; } return the exit status of their command list.


Summary

Bash subshells are about execution environment, not just parentheses syntax. The ( list ) operator runs commands in an isolated environment: cd, variable assignments, exports, and shell-option changes made inside ( … ) do not alter the parent shell. The { list; } reserved-word group runs in the current shell, which is why the list must end with ; or a newline before }.

Bash also creates isolated environments for command substitution, pipeline stages, process substitution, and asynchronous commands.

This explains why variables set inside a while read loop fed by a pipe disappear afterward, and why assignments inside $(…) do not reach the parent shell. Use BASH_SUBSHELL and BASHPID to inspect nesting and process identity, ( … ) when you want to isolate temporary shell-state changes, and { …; } or process substitution when the current shell must keep the changes.

When an external program runs, think child process. The key question is whether Bash is running the commands in the current shell or an isolated environment. That distinction explains why variables, directory changes, and shell settings sometimes disappear after a pipeline, command substitution, or parenthesized group.


References


Frequently Asked Questions

Is { } a subshell?

No. Braces run the command list in the current shell. Only ( ) creates a subshell group in the sense this article uses. Variable assignments and cd inside { …; } persist after the group.

Why did my variable disappear after a pipe?

Each command in a pipeline normally runs in its own execution environment. A while read loop on the right side of | updates variables inside that environment, not in the parent shell. Use process substitution or lastpipe when the loop must modify caller state.

Does $() run in a subshell?

Yes. Standard command substitution runs the nested commands in a subshell. Output is captured as text, but assignments and cd inside the substitution do not affect the parent shell.

Does cd inside () affect the parent?

No. The subshell can change its own working directory, but when the subshell exits the parent shell keeps its previous directory. Use { cd …; …; } when the directory change must remain in effect.

What does lastpipe change?

With shopt -s lastpipe and job control disabled, Bash runs the last pipeline command in the current shell instead of its normal subshell environment. That can let a final while read loop update parent variables. Process substitution is usually clearer in scripts.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)