Bash Streams & Redirection
File descriptors, redirection operators, and stream manipulation.
Stream Fundamentals
# Three standard streams
# stdin (0) - Input
# stdout (1) - Output
# stderr (2) - Errors
# Basic redirection
command > file # stdout to file (overwrite)
command >> file # stdout to file (append)
command 2> file # stderr to file
command 2>> file # stderr append
command < file # stdin from file
# Combine streams
command &> file # stdout AND stderr to file
command &>> file # Both append
command > file 2>&1 # Same as &> (POSIX portable)
command 2>&1 > file # WRONG ORDER: stderr still to terminal!
# Discard output
command > /dev/null # Discard stdout
command 2> /dev/null # Discard stderr
command &> /dev/null # Discard both
# Redirect stderr to stdout for pipeline
command 2>&1 | next_command # Both streams in pipeline
command |& next_command # Bash shorthand for same
Here-Documents and Here-Strings
# Here-document: Multi-line input
cat << 'EOF'
This is literal text.
Variables like $HOME are NOT expanded.
Useful for templates.
EOF
cat << EOF
This text DOES expand variables.
Home is: $HOME
User is: $USER
EOF
# Indent-friendly (<<-)
cat <<- EOF
Tabs at start are stripped.
Content stays aligned.
Great for functions.
EOF
# Here-document for SSH commands
ssh vault-01 << 'EOF'
hostname
uptime
df -h /
EOF
# Here-document for config files
cat > /tmp/nginx.conf << 'EOF'
server {
listen 80;
server_name example.com;
root /var/www/html;
}
EOF
# Here-string: Single-line input
grep "pattern" <<< "search in this string"
while read -r word; do
echo "Word: $word"
done <<< "one two three"
# Variable content to stdin
json='{"key": "value"}'
jq '.key' <<< "$json"
# Command output as input
bc <<< "scale=2; 100/3"
# Here-document in function
create_service_file() {
local name="$1"
local exec="$2"
cat << EOF
[Unit]
Description=$name Service
After=network.target
[Service]
ExecStart=$exec
Restart=always
[Install]
WantedBy=multi-user.target
EOF
}
create_service_file "MyApp" "/usr/bin/myapp" > /tmp/myapp.service
Custom File Descriptors
# Open file descriptor for reading (3-9 are free)
exec 3< /etc/passwd
while read -r line <&3; do
echo "$line"
done
exec 3<&- # Close descriptor
# Open for writing
exec 4> /tmp/output.log
echo "Log entry 1" >&4
echo "Log entry 2" >&4
exec 4>&- # Close
# Open for read/write
exec 5<> /tmp/data.txt
read -r line <&5 # Read
echo "appended" >&5 # Write
exec 5>&-
# Duplicate stdin/stdout for restoration
exec 3>&1 # Save stdout to fd 3
exec 1> /tmp/output.log # Redirect stdout
echo "This goes to file"
exec 1>&3 # Restore stdout
exec 3>&- # Close fd 3
echo "This goes to terminal"
# Swap stdout and stderr
command 3>&1 1>&2 2>&3 3>&-
# Log to file while preserving terminal output
exec 3>&1 4>&2 # Save original stdout/stderr
exec 1> >(tee -a /tmp/script.log) 2>&1 # Redirect with tee
echo "This logs and displays"
exec 1>&3 2>&4 # Restore
exec 3>&- 4>&- # Close
# Multiple log files
exec 3>> /var/log/app/info.log
exec 4>> /var/log/app/error.log
echo "Info message" >&3
echo "Error message" >&4
exec 3>&- 4>&-
Process Substitution for Streams
# <() creates readable pseudo-file from command
diff <(cat file1) <(cat file2)
# Compare remote files
diff <(ssh vault-01 cat /etc/hosts) <(ssh vault-02 cat /etc/hosts)
# Compare sorted
diff <(sort file1.txt) <(sort file2.txt)
# >() creates writable pseudo-file
command | tee >(gzip > output.gz)
# Fan-out to multiple processors
generate_data | tee >(process_a > a.out) >(process_b > b.out) > combined.out
# Log levels to separate files
./app 2>&1 | tee >(grep ERROR >> errors.log) >(grep WARN >> warnings.log)
# Feed multiple commands from same source
<(cat file.txt) >(grep pattern1) >(grep pattern2)
# Avoid subshell with process substitution
count=0
while read -r line; do
((count++))
done < <(cat /etc/passwd) # No subshell!
echo "Count: $count" # Correct value
Advanced Redirection Patterns
# Redirect for entire script
exec > /var/log/myscript.log 2>&1
echo "All output goes to log file"
# Redirect for code block
{
echo "Line 1"
echo "Line 2"
echo "Line 3"
} > output.txt
# Redirect for loop
for host in vault-01 ise-01 bind-01; do
echo "Checking $host..."
ping -c1 "$host"
done > hosts_status.log 2>&1
# Conditional redirection
verbose=true
if $verbose; then
exec 3>&1 # Normal stdout
else
exec 3> /dev/null # Discard
fi
echo "Maybe visible" >&3
# Redirect stdin for entire script
exec < /etc/passwd
while read -r line; do
echo "$line"
done
# Read password without echo (terminal control)
read -s -p "Password: " password
echo "" # Newline after hidden input
# Redirect from string
read -r name age <<< "John 25"
echo "Name: $name, Age: $age"
# Append with sudo (can't just sudo >>)
echo "new line" | sudo tee -a /etc/myconfig > /dev/null
Coprocesses (Bidirectional Communication)
# Start coprocess
coproc my_coproc { bc -l; }
# Write to coprocess stdin
echo "scale=4; 22/7" >&"${my_coproc[1]}"
# Read from coprocess stdout
read -r result <&"${my_coproc[0]}"
echo "Result: $result" # 3.1428
# Close and wait
exec {my_coproc[1]}>&-
wait "$my_coproc_PID"
# Coprocess with named pipes
coproc MATH { bc -l; }
# Multiple calculations
for expr in "2+2" "10/3" "sqrt(2)"; do
echo "scale=4; $expr" >&"${MATH[1]}"
read -r result <&"${MATH[0]}"
echo "$expr = $result"
done
exec {MATH[1]}>&-
# Interactive session
coproc SSH { ssh -tt vault-01; }
echo "hostname" >&"${SSH[1]}"
read -r hostname <&"${SSH[0]}"
echo "Host is: $hostname"
exec {SSH[1]}>&-
Infrastructure Stream Patterns
# Session logging with timestamps
exec > >(while read -r line; do echo "$(date '+%Y-%m-%d %H:%M:%S') $line"; done | tee -a /var/log/session.log) 2>&1
# Separate stdout/stderr logs
exec 1> >(tee -a /var/log/app/stdout.log)
exec 2> >(tee -a /var/log/app/stderr.log >&2)
# Configuration from heredoc
cat << 'EOF' | ssh vault-01 "sudo tee /etc/vault.d/config.hcl > /dev/null"
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = false
}
storage "raft" {
path = "/opt/vault/data"
}
EOF
# Parallel command output to separate files
for host in vault-01 ise-01 bind-01; do
ssh "$host" "df -h /" > "/tmp/${host}_disk.txt" 2>&1 &
done
wait
# Feed dACL content to netapi
netapi ise ers dacl create --name "Linux-AD-Auth" --content - << 'EOF'
permit udp any any eq domain
permit tcp any any eq domain
permit udp any host ${AD_IP} eq 88
permit tcp any host ${AD_IP} eq 88
permit udp any host ${AD_IP} eq 123
deny ip any any
EOF
# k8s secrets from heredoc
kubectl create secret generic vault-config --from-file=config.hcl=/dev/stdin << 'EOF'
listener "tcp" {
address = "0.0.0.0:8200"
}
EOF
# Multi-line git commit message
git commit -m "$(cat << 'EOF'
feat(auth): Add LDAP authentication
- Integrate with Active Directory
- Support nested groups
- Add caching for performance
EOF
)"
Error Stream Handling
# Capture stderr only
errors=$(command 2>&1 >/dev/null)
# Capture stdout and stderr separately
{ stdout=$(command 2>&1 1>&3 3>&-); } 3>&1
stderr=$(command 2>&1 >/dev/null)
# Check if command produced errors
if stderr=$(command 2>&1 >/dev/null) && [[ -z "$stderr" ]]; then
echo "No errors"
else
echo "Errors: $stderr"
fi
# Log errors with context
run_with_logging() {
local cmd="$*"
local output
local exit_code
output=$("$@" 2>&1)
exit_code=$?
if (( exit_code != 0 )); then
echo "[ERROR] Command failed: $cmd" >&2
echo "[ERROR] Exit code: $exit_code" >&2
echo "[ERROR] Output: $output" >&2
fi
return $exit_code
}
# Conditional error display
command 2>&1 | grep -v "expected warning"
# Prefix stderr lines
command 2> >(sed 's/^/[stderr] /' >&2)
# Timestamped error log
exec 2> >(while read -r line; do
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $line"
done >> /var/log/errors.log)
Stream Gotchas
# WRONG: Order matters!
command 2>&1 > file # stderr to terminal, stdout to file
command > file 2>&1 # CORRECT: Both to file
# WRONG: Redirect in wrong place
sudo echo "text" > /etc/protected # Redirection runs as user, not root!
# CORRECT: Use tee
echo "text" | sudo tee /etc/protected > /dev/null
# WRONG: Lost file descriptor
{
exec 3> /tmp/out
echo "test" >&3
} # fd 3 closed at block end!
# CORRECT: Manage scope properly
exec 3> /tmp/out
echo "test" >&3
exec 3>&-
# WRONG: Here-document in subshell
(
cat << EOF
$var is expanded in subshell
EOF
) # $var might not be what you expect
# WRONG: Expecting read from closed descriptor
exec 3< file.txt
cat <&3 # Reads entire file
read -r line <&3 # Nothing left to read!
# CORRECT: Seek not possible, reopen
exec 3< file.txt
read -r line <&3
exec 3< file.txt # Reopen to "reset"
# WRONG: Buffering in pipeline to file
tail -f log | grep pattern > matches.txt # Buffered, delayed writes!
# CORRECT: Unbuffered
tail -f log | stdbuf -oL grep pattern > matches.txt
# WRONG: Here-doc in function loses stdin
myfunc() {
cat << EOF
Some content
EOF
read -r input # Can't read! stdin consumed by heredoc
}
# CORRECT: Use fd 0 explicitly if needed
myfunc() {
cat << EOF
Some content
EOF
read -r input <&0 # Still won't work - stdin is heredoc
}