14.3. Shell Options and Subshells#

14.3.1. Common Pitfalls#

1. Subshell variables don’t affect parent

# Bad: Expecting variables to persist
(count=10)
echo $count  # Empty, not 10

# Good: Use global scope if needed
count=10
{
  ((count++))
}
echo $count  # 11

2. Combining set options incorrectly

# Bad: set -e causes issues with conditionals
set -e
if some_command; then
  echo "Success"
fi  # Script exits if some_command fails

# Better: Don't use with conditionals
set -e
some_command  # Script exits if fails
echo "Success"

# Or disable for specific commands
set +e
some_command  # Doesn't exit even if fails
set -e

3. Glob expansion surprises

# Bad: Filenames with spaces break
shopt -s nullglob
for file in *.txt; do
  echo "$file"  # May break if spaces (use quotes!)
done

# Good: Always quote
for file in *.txt; do
  echo "$file"  # Works with spaces if quoted
done

4. Process substitution not supported in older Bash

# Not available in Bash < 3.0
diff <(ls dir1) <(ls dir2)  # May not work

# Portable alternative
diff <(ls dir1 | sort) <(ls dir2 | sort)
# Or use temp files
ls dir1 > /tmp/dir1.txt
ls dir2 > /tmp/dir2.txt
diff /tmp/dir1.txt /tmp/dir2.txt

5. Pipefail with set -e interaction

# Confusing: -e waits for pipe to finish
set -e
cat file.txt | head -1 | tail -1  # Returns 0 if tail succeeds (not head)

# Better: Use set -o pipefail
set -o pipefail
cat file.txt | grep error  # Fails if grep finds nothing

14.3.2. Subshells and Process Substitution#

Subshells create isolated execution contexts, useful for cleanup and parallelism.

14.3.2.1. Subshell Basics#

#!/bin/bash

# Subshell with ()
echo "Parent PID: $$"
(echo "Subshell PID: $$")  # Different PID

# Variables don't leak from subshells
var="parent"
(var="child"; echo $var)  # child
echo $var  # parent

# Subshell with { }
{ var="child"; echo $var; }  # Runs in same shell (no subshell)

# cd in subshell doesn't affect parent
pwd  # /home/user
(cd /tmp; pwd)  # /tmp
pwd  # /home/user (unchanged)

# Combining operations in subshell
(
  cd /var/log
  ls -la
  grep error messages.log
) > /tmp/log_analysis.txt

# Background subshell
(
  sleep 10
  echo "Task complete"
) &

# Wait for background job
wait $!

14.3.2.2. Process Substitution#

#!/bin/bash

# Process substitution: <(command) creates file descriptor
diff <(ls dir1) <(ls dir2)  # Compare directory contents

# Avoid temporary files
cat > config.txt << 'EOF'
file1
file2
file3
EOF

# Compare config with current files
comm -23 <(cat config.txt | sort) <(ls | sort)

# Multiple inputs
paste <(seq 1 5) <(echo A; echo B; echo C; echo D; echo E)

# Pipeline with multiple inputs
sort <(cat file1) <(cat file2) | uniq -d  # Common lines

# Redirect multiple commands' output
cat <(echo "### Header") <(cat data.txt) > output.txt

# Process substitution in loops
while IFS= read -r file; do
  echo "$file"
done < <(find . -type f -name "*.txt")

14.3.2.3. Subshell Use Cases#

#!/bin/bash

# Cleanup with trap in subshell
process_with_cleanup() {
  local temp_dir
  temp_dir=$(mktemp -d)
  
  (
    trap "rm -rf $temp_dir" EXIT
    
    # Work in temp_dir
    cp input.txt "$temp_dir/"
    process "$temp_dir/input.txt"
    # Automatic cleanup on exit
  )
}

# Parallel processing with subshells
declare -a pids=()
for file in *.log; do
  (
    echo "Processing $file..."
    analyze_log "$file"
  ) &
  pids+=($!)
done

# Wait for all
for pid in "${pids[@]}"; do
  wait $pid || echo "Process $pid failed"
done

# Isolate environment changes
(
  set -e
  set -u
  cd /var/log
  # environment changes are isolated
  exec > /tmp/output.log 2>&1
)
# Parent shell environment unchanged

14.3.3. Shell Options (shopt)#

The shopt command controls optional Bash features, affecting script behavior and debugging.

14.3.3.1. Common Shell Options#

#!/bin/bash

# Enable option
shopt -s nullglob      # Non-matching globs expand to empty (not literal)
shopt -s extglob       # Extended pattern matching
shopt -s dotglob       # Include dotfiles in glob expansion

# Disable option
shopt -u nocaseglob    # Case-sensitive globbing
shopt -u huponexit     # Don't HUP when shell exits

# Check option status
shopt -p nullglob      # Print if set or unset

# List all options
shopt                  # Show all with status

# Practical options for robust scripts
#!/bin/bash
set -euo pipefail      # Exit on error, undefined vars, pipe failure
shopt -s nullglob      # Don't error on missing glob patterns
shopt -s failglob      # Error if glob doesn't match
shopt -s dotglob       # Include hidden files in globbing

14.3.3.2. Essential Script Options with set#

#!/bin/bash

# set -e: Exit if any command fails
set -e
gcc program.c || echo "Compilation failed"  # Script exits here
echo "This won't run"

# set -u: Error on undefined variables
set -u
echo "$UNDEFINED_VAR"  # Error: parameter not set

# set -o pipefail: Return last non-zero exit from pipe
set -o pipefail
cat nonexistent.txt | grep pattern  # Returns error, not 0

# set -x: Print commands before execution (debugging)
set -x
ls -la  # Output shows: + ls -la
ls  # Output shows: + ls

# Combine multiple options
set -euxo pipefail  # All of the above

# Unset options
set +e  # Disable exit-on-error
set +u  # Disable undefined variable check

14.3.3.3. Scoping and SHELLOPTS#

#!/bin/bash

# Current shell options are in SHELLOPTS
echo $SHELLOPTS  # Shows current options

# Options in subshell
(
  set -x  # Only affects this subshell
  echo "This is printed with +x"
)
# Back in parent shell, -x is not set

# Propagate options to subshells
export SHELLOPTS
(
  echo "Has parent's options"
)

# Get option status programmatically
check_option() {
  local opt=$1
  local status=$(shopt -s "$opt" 2>/dev/null && echo "on" || echo "off")
  echo "Option $opt is $status"
}

check_option nullglob
check_option dotglob