Bash scripting guide · part 25 of 32
Subshells and command grouping
The difference between ( ... ) and { ...; }, what a subshell inherits and what it cannot change, which constructs create one, and what that costs.
A subshell is a copy of the current shell running in a child process. Bash creates it with
fork(), so the child starts with everything the parent had: all variables (exported or
not), functions, aliases, options, the current directory and open file descriptors. Traps
are the exception: handlers are reset to the default, and only signals set to be ignored stay
ignored. Whatever the child changes stays in the child. When it exits, the parent
continues exactly as it was.
Two pieces of syntax group commands:
( list )runs the list in a subshell.{ list; }runs the list in the current shell; it only groups.
Changes inside ( ) do not leak out
Variables and the directory are restored
x=1
( x=2; cd /tmp; echo "inside: x=$x pwd=$PWD" )
echo "outside: x=$x pwd=$PWD"
{ x=3; cd data; }
echo "after { }: x=$x pwd=$PWD"
inside: x=2 pwd=/tmp
outside: x=1 pwd=/home/dev/demo
after { }: x=3 pwd=/home/dev/demo/data The parentheses changed x and the directory in the child only; the parent still has
x=1 and its old directory. The braces ran in the current shell, so their changes stuck.
That isolation is the main reason to use a subshell on purpose: run a few commands in another directory, or with different options, without having to undo anything afterwards.
Work in another directory without cd back
( cd logs && wc -l *.log )
pwd
12 app.log 2 error.log 14 total /home/dev/demo
cd logs applied only inside the parentheses, and && means the wc would not run in the
wrong place if the cd failed. The script stays in /home/dev/demo with no cd - needed.
Subshell versus child process
A subshell is not the same as running bash again. The subshell is a fork of the current
shell and inherits even unexported variables. A new bash (or any script you start) only
receives the environment, which is the exported variables.
What a subshell sees that a new bash does not
secret=42
( echo "subshell sees: [$secret]" )
bash -c 'echo "new bash sees: [$secret]"'
export secret
bash -c 'echo "after export: [$secret]"'
subshell sees: [42] new bash sees: [] after export: [42]
The subshell saw the plain shell variable; the separate bash process did not until the
variable was exported. See Environment variables.
Telling them apart: $$, BASHPID and BASH_SUBSHELL
$$ is the PID of the main shell and keeps that value in subshells, by design, so that
temp-file names built from it stay consistent. $BASHPID is the PID of the process actually
running, and $BASH_SUBSHELL counts how deeply nested you are.
The PID variables inside nested subshells
echo "main: \$\$=$$ BASHPID=$BASHPID level=$BASH_SUBSHELL"
( echo "sub: \$\$=$$ BASHPID=$BASHPID level=$BASH_SUBSHELL"
( echo "subsub: \$\$=$$ BASHPID=$BASHPID level=$BASH_SUBSHELL" ) )
main: $$=5130 BASHPID=5130 level=0 sub: $$=5130 BASHPID=5131 level=1 subsub: $$=5130 BASHPID=5132 level=2
$$ is the same on every line; BASHPID changes in each new process, and the level goes
0, 1, 2. The PIDs vary from run to run.
exit in a subshell exits only the subshell
exit inside ( ) ends the child, and its status becomes the status of the parentheses.
Inside { } it ends the whole script. The same holds for set -e: a failure inside a
subshell ends the subshell, and the parent decides what to do with its status.
exit inside ( ) and inside { }
( echo "in subshell"; exit 3 )
echo "subshell returned $?"
{ echo "in group"; exit 4; }
echo "never printed"
in subshell subshell returned 3 in group
The subshell's exit 3 only set $? for the parent, which carried on. The braces ran
in the main shell, so exit 4 ended the script with status 4. A common idiom relies on
this: cmd || { echo "failed" >&2; exit 1; }.
The { } syntax is picky
{ and } are reserved words, not operators like (. They must be separate words, and the
closing brace must come where a new command could start, which means after a newline or ;.
Missing space or semicolon
{echo hi}
echo "---"
{ echo a; echo b }
echo "this line is never reached"
/bin/bash: line 1: {echo: command not found
---
/bin/bash: -c: line 5: syntax error: unexpected end of file {echo is a single word, so bash looked for a command called {echo and failed. In the
second group, } without a preceding ; is just an argument to echo, the group is never
closed, and bash hits end of input with a syntax error. Write { echo a; echo b; }.
Grouping for redirection and control flow
Both forms take redirections that apply to everything inside, and both can be the target of
&&, || or a pipe. Prefer braces when you do not need isolation: they do not cost a fork.
Redirect a group and handle failure in one place
{ echo "# hosts"; sort data/hosts.txt; } > hosts.sorted
cat hosts.sorted
grep -q db01 data/hosts.txt && { echo "db01 is listed"; echo "checking it next"; }
# hosts cache01 10.0.0.31 db01 10.0.0.21 web01 10.0.0.11 web02 10.0.0.12 db01 is listed checking it next
The header and the sorted lines land in one file through a single redirection. After
&&, the braces make both echo commands conditional; without them only the first would
be.
Constructs that create a subshell without parentheses
- Every command of a pipeline except, with
shopt -s lastpipe, the last (Pipes and pipelines). - Command substitution
$(...)and backticks (Command substitution). - Process substitution
<(...)and>(...)(Process substitution). - A command run in the background with
&, including{ ...; } &. - Coprocesses (
coproc).
Everything said above applies to them: a variable assigned inside $(...) or on the right
side of a pipe is gone afterwards.
What forking costs
A fork is cheap by process standards but expensive compared to the shell's own work. In a
loop that runs thousands of times, replacing $(...) with a builtin or parameter expansion
can make a script many times faster.
1000 command substitutions versus none
time for i in $(seq 1000); do x=$(echo "$i"); done
time for i in $(seq 1000); do x=$i; done
real 0m0.518s user 0m0.275s sys 0m0.303s real 0m0.002s user 0m0.001s sys 0m0.001s
time reports the elapsed (real) and CPU times for each loop. The first forks a subshell
on every iteration and takes far longer than the second, which never leaves the current
shell. The exact numbers depend on the machine and vary between runs.
Related
execReplace the shell with another program, or, with only redirections, change where the shell's own input and output go.exportMark shell variables (or bash functions) to be passed to every program the shell starts.- Pipes and pipelinesHow cmd1 | cmd2 connects processes, which exit status a pipeline returns, PIPESTATUS, pipefail, SIGPIPE, and why variables set in a piped loop vanish.
- Command substitutionCapturing a command's output with $(...), what happens to trailing newlines and stderr, how to check the exit status, and the local trap that hides failures.
- Process substitutionUse <(cmd) and >(cmd) to hand a command's output to a program that wants a file name, keep while-read loops in the current shell, and avoid its timing traps.
- FunctionsDefining and calling bash functions, arguments, local variables, return status versus output, namerefs, and sourcing a function library.
- Environment variablesHow export passes variables to child processes, the one-command NAME=value prefix, loading .env files, and why a child cannot change its parent.