Subshells¶
Understanding subshells is crucial for avoiding bugs related to variable scope and process isolation.
What is a Subshell?¶
A subshell is a child process that runs a copy of the current shell. It inherits:
- Environment variables (exported)
- Current directory
- File descriptors
It does NOT share:
- Shell variables (non-exported)
- Shell options
- Function definitions (unless exported)
Creating Subshells¶
Explicit Subshell with ()¶
Commands run in a separate process:
var="original"
(var="changed"; echo "Inside: $var") # Inside: changed
echo "Outside: $var" # Outside: original
The change doesn't persist.
Directory Change in Subshell¶
Useful for temporary directory operations.
Command Substitution¶
The command runs in a subshell:
var="outer"
result=$(var="inner"; echo "$var")
echo "$result" # inner
echo "$var" # outer (unchanged)
Pipes Create Subshells¶
Each part of a pipeline runs in a subshell:
var="original"
echo "data" | {
read line
var="changed"
echo "Inside pipe: $var" # Inside pipe: changed
}
echo "Outside: $var" # Outside: original
This is a common source of bugs!
The Pipe Subshell Problem¶
The Bug¶
count=0
cat file.txt | while read line; do
((count++))
done
echo "Lines: $count" # Lines: 0 (wrong!)
The while loop runs in a subshell, so count changes are lost.
Solutions¶
Process Substitution¶
Here String¶
Redirect from File¶
lastpipe Option (Bash 4.2+)¶
shopt -s lastpipe
count=0
cat file.txt | while read line; do
((count++))
done
echo "Lines: $count" # Works with lastpipe
lastpipe Requirement
lastpipe only works when job control is disabled (non-interactive shells or set +m).
Process Substitution¶
Input Process Substitution <()¶
Treat command output as a file:
The <(command) creates a file descriptor that reads from the command's output.
Output Process Substitution >()¶
Send output to a command as if to a file:
Practical Examples¶
# Compare directory listings
diff <(ls dir1) <(ls dir2)
# Join on processed data
join <(sort -k1 file1) <(sort -k1 file2)
# Multiple outputs
command | tee >(gzip > output.gz) | head
Subshell vs Brace Group¶
Subshell ()¶
Runs in separate process:
Brace Group {}¶
Runs in current shell:
When to Use Which¶
Use subshell () when you want:
- Isolated environment
- Temporary directory change
- Parallel execution
Use brace group {} when you want:
- Grouping for redirection
- Variable changes to persist
- Conditional execution
# Redirect group
{ echo "header"; cat file; echo "footer"; } > output.txt
# Conditional group
[[ -f file ]] && { process; cleanup; notify; }
Detecting Subshells¶
echo "Current shell PID: $$"
echo "Actual PID: $BASHPID"
(echo "Subshell BASHPID: $BASHPID") # Different from $$
$$ is the parent shell PID (constant), $BASHPID is the actual current process.
Environment vs Shell Variables¶
Shell Variables (Not Inherited)¶
Environment Variables (Inherited)¶
export myvar="hello"
(echo "$myvar") # hello
# Or inline export
myvar="hello" bash -c 'echo "$myvar"' # hello
Functions and Subshells¶
Functions Not Available¶
Export Functions¶
Common Patterns¶
Isolated Operations¶
# Temporary environment
(
export PATH="/custom/path:$PATH"
export DEBUG=1
./script.sh
)
# Original environment unchanged
Parallel Execution¶
Safe Directory Operations¶
# Process in different directory without cd-ing
(cd /data && tar -czf backup.tar.gz *)
# Extract to specific location
(cd /target && tar -xzf /path/to/archive.tar.gz)
Error Isolation¶
# Errors in subshell don't exit parent
(
set -e
risky_command
another_command
) || echo "Subshell failed but we continue"
Capture Output with Side Effects¶
# Get output and exit code
output=$(command; echo "::$?")
exit_code="${output##*::}"
output="${output%::*}"
Performance Considerations¶
Subshells have overhead (process creation). Avoid in tight loops:
# Slow - subshell per iteration
for i in {1..1000}; do
result=$(echo "$i * 2" | bc)
done
# Fast - no subshell
for i in {1..1000}; do
((result = i * 2))
done
Try It¶
-
Variable scope:
-
Pipe problem:
-
Process substitution:
-
Subshell PID:
Summary¶
| Construct | Creates Subshell | Variables Persist |
|---|---|---|
(commands) | Yes | No |
$(command) | Yes | No |
cmd1 \| cmd2 | Yes (both sides) | No |
{ commands; } | No | Yes |
< <(command) | Yes (for command) | Yes (for loop) |
Common pitfalls:
- Variables set in pipes don't persist
- Functions need
export -ffor subshells $$vs$BASHPIDdiffer in subshells- Subshells have process creation overhead
Solutions:
- Use process substitution
< <(cmd)instead of pipes - Use
lastpipeoption (Bash 4.2+) - Use brace groups
{}when persistence needed - Export functions for subshell access