Lesson 16: Pipes, Redirection & Chaining
Goal: Master the Unix philosophy — connect small tools to build powerful workflows.
Table of Contents
- Standard Streams
- Output Redirection — > and >>
- Input Redirection — <
- Error Redirection — 2>
- Pipes — |
- tee — Split Output
- xargs — Arguments from Stdin
- Command Chaining — &&, ||, ;
- Here Documents
- Practical Pipelines
- Exercises
Standard Streams
Every Linux process has three standard streams:
┌──────────┐
stdin (0) ──────>│ │──────> stdout (1)
(keyboard) │ Process │
│ │──────> stderr (2)
└──────────┘ (errors)
| Stream | Number | Default | Description |
|---|---|---|---|
| stdin | 0 | Keyboard | Input |
| stdout | 1 | Terminal | Normal output |
| stderr | 2 | Terminal | Error output |
Output Redirection — > and >>
Write to a file (overwrite)
# Redirect stdout to a file
echo "Hello" > output.txt
# Command output to a file
ls -la > filelist.txt
# Date to a file
date > timestamp.txt
Append to a file
# Append to existing file
echo "Line 1" > log.txt
echo "Line 2" >> log.txt
echo "Line 3" >> log.txt
cat log.txt
# Line 1
# Line 2
# Line 3
Overwrite vs append
# > overwrites (dangerous — old content is gone!)
echo "new content" > file.txt
# >> appends (safe — old content preserved)
echo "new line" >> file.txt
Input Redirection — <
# Read from a file instead of keyboard
wc -l < /etc/passwd
# Sort a file
sort < unsorted.txt > sorted.txt
# Send mail from a file
mail user@example.com < message.txt
Error Redirection — 2>
Redirect errors to a file
# Send errors to a file
find / -name "*.conf" 2> errors.log
# Discard errors completely
find / -name "*.conf" 2>/dev/null
Redirect stdout and stderr separately
# stdout to one file, stderr to another
command > output.log 2> error.log
Redirect both to the same file
# Method 1: &> (bash shorthand)
command &> all.log
# Method 2: explicit
command > all.log 2>&1
Redirect stderr to stdout (merge)
# Useful for piping — errors also go through the pipe
command 2>&1 | grep "something"
Practical example
# Run a script, save all output, and still see it on screen
./deploy.sh 2>&1 | tee deploy.log
Pipes — |
The pipe | sends the output of one command as input to the next.
command1 | command2 | command3
Basic examples
# Count files in a directory
ls | wc -l
# Find a running process
ps aux | grep nginx
# Sort and remove duplicates
cat names.txt | sort | uniq
# Show disk usage, sorted by size
du -h --max-depth=1 | sort -rh
# Show the 5 largest files
ls -lS | head -5
Building pipelines
Think of each command as a step:
# Step by step:
# 1. Read the file
# 2. Extract column 1 (usernames)
# 3. Sort them
# 4. Count duplicates
# 5. Sort by count
# 6. Show top 10
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
tee — Split Output
tee writes output to both a file AND stdout (like a T-pipe).
# Save to file AND display on screen
ls -la | tee filelist.txt
# Append instead of overwrite
ls -la | tee -a filelist.txt
# Save to multiple files
echo "Hello" | tee file1.txt file2.txt file3.txt
Practical uses
# Log a build while watching it
make 2>&1 | tee build.log
# Run and log a script
./deploy.sh 2>&1 | tee "deploy-$(date +%Y%m%d-%H%M).log"
# Write to a protected file with sudo
echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts
xargs — Arguments from Stdin
xargs takes input from stdin and converts it into arguments for another command.
Basic usage
# Delete all .tmp files found by find
find . -name "*.tmp" | xargs rm
# Same with safety for filenames with spaces
find . -name "*.tmp" -print0 | xargs -0 rm
With placeholder
# Use {} as placeholder for each input item
echo "file1 file2 file3" | xargs -I {} touch {}.txt
# Copy files to a backup directory
find . -name "*.conf" | xargs -I {} cp {} /backup/
Limit parallel execution
# Process one item at a time
cat urls.txt | xargs -n 1 curl -O
# Process 4 items in parallel
cat urls.txt | xargs -P 4 -n 1 curl -O
Practical examples
# Kill all processes matching a name
pgrep -f "old-app" | xargs kill
# Compress all .log files
find /var/log -name "*.log" -mtime +7 | xargs gzip
# Count lines in all Python files
find . -name "*.py" | xargs wc -l
# Remove all Docker containers
docker ps -aq | xargs docker rm
Command Chaining — &&, ||, ;
Sequential execution with ;
Run commands in sequence regardless of success/failure:
echo "First" ; echo "Second" ; echo "Third"
Conditional AND — &&
Run next command ONLY if previous succeeds:
# Only upgrade if update succeeds
sudo apt update && sudo apt upgrade
# Only start if build succeeds
npm run build && npm start
# Chain multiple steps
mkdir project && cd project && git init && npm init -y
Conditional OR — ||
Run next command ONLY if previous fails:
# Show error message if command fails
cd /nonexistent || echo "Directory not found!"
# Fallback behavior
which nala || echo "nala not installed, using apt"
# Create directory if it doesn't exist
test -d /tmp/mydir || mkdir /tmp/mydir
Combining && and ||
# If-then-else pattern
test -f config.yaml && echo "Config found" || echo "Config missing!"
# Practical: check and act
ping -c 1 google.com > /dev/null 2>&1 && echo "Online" || echo "Offline"
Here Documents
Write multi-line input to a command.
# Create a file with content
cat > config.txt << EOF
server=localhost
port=8080
debug=true
EOF
# Use variables in heredoc (double-quoted behavior)
cat > greeting.txt << EOF
Hello, $USER!
Today is $(date).
EOF
# Literal heredoc (no variable expansion)
cat > template.txt << 'EOF'
Hello, $USER!
This $variable is not expanded.
EOF
Practical Pipelines
System administration
# Top 10 processes by memory
ps aux --sort=-%mem | head -11
# Failed SSH login attempts
grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head -10
# Monitor disk space every 5 seconds
watch -n 5 'df -h | grep -E "^/dev"'
# Active network connections summary
ss -tulnp | awk 'NR>1 {print $1}' | sort | uniq -c
Log analysis
# HTTP status code distribution
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# Requests per hour
awk '{print $4}' access.log | cut -d: -f1,2 | sort | uniq -c
# Slowest requests
awk '$NF > 5 {print $7, $NF}' access.log | sort -k2 -rn | head -10
File management
# Find duplicate files by checksum
find . -type f -exec md5sum {} + | sort | uniq -d -w32
# Rename all .jpeg to .jpg
find . -name "*.jpeg" | while read f; do mv "$f" "${f%.jpeg}.jpg"; done
# Bulk create dated backups
ls *.conf | xargs -I {} cp {} "{}.$(date +%Y%m%d).bak"
Exercises
Exercise 1: Redirection
# 1. Save ls output to a file
ls -la /etc > /tmp/etc-contents.txt
# 2. Append the date
date >> /tmp/etc-contents.txt
# 3. Count lines (using input redirection)
wc -l < /tmp/etc-contents.txt
# 4. Search with errors suppressed
find / -name "*.conf" 2>/dev/null | head -10
# 5. Clean up
rm /tmp/etc-contents.txt
Exercise 2: Pipes
# 1. Count unique shells used on the system
cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn
# 2. Find the 5 largest items in /var
sudo du -sh /var/* 2>/dev/null | sort -rh | head -5
# 3. Count running processes per user
ps aux | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
Exercise 3: Chaining
# 1. Create a directory and enter it (fails gracefully)
mkdir /tmp/test-chain && cd /tmp/test-chain && echo "Success!" || echo "Failed!"
# 2. Check if a command exists
which docker && echo "Docker is installed" || echo "Docker is NOT installed"
# 3. Conditional file creation
test -f /tmp/marker.txt || echo "Created" > /tmp/marker.txt
cat /tmp/marker.txt
rm /tmp/marker.txt
Key Takeaways
>overwrite,>>append,<input from file2>redirect errors,2>/dev/nulldiscard errors|pipe output → input (the Unix superpower)tee— save to file AND displayxargs— convert stdin to command arguments&&— run next only if previous succeeds||— run next only if previous fails- Build complex workflows from simple tools
Next Lesson: Lesson 17: Shell Configuration →