Running commands one after another, checking if they succeed, and handling failures gracefully is a daily task for developers. Most people use if statements for everything, but command chaining operators can make your scripts cleaner, faster, and more elegant.
The Problem
You need to run a series of commands, but only continue if each succeeds. The verbose approach:
cd /var/www/app
if [ $? -eq 0 ]; then
git pull origin main
if [ $? -eq 0 ]; then
npm install
if [ $? -eq 0 ]; then
npm run build
if [ $? -eq 0 ]; then
pm2 restart app
fi
fi
fi
fiNested if statements everywhere. Hard to read, hard to maintain.
The Hack: Command Chaining
Use operators to chain commands based on success or failure:
cd /var/www/app && \
git pull origin main && \
npm install && \
npm run build && \
pm2 restart appOne clean chain. Each command runs only if the previous one succeeds!
The Three Operators
AND Operator (&&)
Run the next command only if the previous one succeeded (exit code 0):
# Basic usage
mkdir project && cd project
# Multiple commands
command1 && command2 && command3
# Real example
apt-get update && apt-get upgrade -y && apt-get autoremove -yOR Operator (||)
Run the next command only if the previous one failed (non-zero exit code):
# Fallback behavior
command1 || command2
# Try primary, fallback to secondary
wget https://example.com/file.tar.gz || curl -O https://example.com/file.tar.gz
# Error handling
./script.sh || echo "Script failed!" && exit 1Semicolon (;)
Run commands sequentially regardless of success or failure:
# Always run all commands
command1 ; command2 ; command3
# Cleanup regardless of outcome
./run-tests.sh ; rm -rf /tmp/test-*Practical Examples
Deployment Pipeline
#!/bin/bash
# Deploy application with proper error handling
echo "Starting deployment..." && \
cd /var/www/app && \
git fetch origin && \
git reset --hard origin/main && \
npm ci && \
npm run build && \
npm run test && \
pm2 restart app && \
echo "Deployment successful!" || \
echo "Deployment failed at step: $?"Backup with Fallbacks
#!/bin/bash
# Try multiple backup methods
rsync -avz /data/ backup-server:/backups/ || \
scp -r /data/ backup-server:/backups/ || \
tar czf /tmp/backup.tar.gz /data/ || \
echo "All backup methods failed!" && exit 1Database Operations
#!/bin/bash
# Backup before migration
pg_dump mydb > backup.sql && \
psql mydb < migration.sql && \
echo "Migration successful" || \
(echo "Migration failed, restoring backup..." && \
psql mydb < backup.sql && \
echo "Backup restored")Service Health Check
#!/bin/bash
# Check if service is running, start if not
systemctl is-active --quiet nginx || \
(echo "Nginx is down, starting..." && \
systemctl start nginx && \
echo "Nginx started") || \
echo "Failed to start Nginx"Advanced Patterns
Combining Operators
# Try command, log success or failure
./script.sh && echo "Success" || echo "Failed"
# Create directory and file, or cleanup on failure
mkdir -p /tmp/work && touch /tmp/work/file.txt || rm -rf /tmp/work
# Complex logic
command1 && (command2 || command3) && command4Multi-line Chains
#!/bin/bash
# Use backslash for readability
cd /project && \
git pull && \
npm install && \
npm test && \
npm run build && \
echo "Build complete"
# Or group in parentheses
(
cd /project &&
git pull &&
npm install &&
npm test
) && echo "Tests passed" || echo "Tests failed"Conditional Execution Based on Files
# Run only if file exists
[ -f config.yml ] && ./app --config config.yml
# Run if file doesn't exist
[ ! -f data.db ] && ./init-database.sh
# Check multiple conditions
[ -f Dockerfile ] && [ -f docker-compose.yml ] && docker-compose up -dReal-World Use Cases
CI/CD Pipeline
#!/bin/bash
set -e # Exit on any error
echo "Running CI/CD pipeline..." && \
\
echo "Step 1: Linting..." && \
npm run lint && \
\
echo "Step 2: Unit tests..." && \
npm run test:unit && \
\
echo "Step 3: Integration tests..." && \
npm run test:integration && \
\
echo "Step 4: Build..." && \
npm run build && \
\
echo "Step 5: Deploy..." && \
./deploy.sh && \
\
echo "Pipeline completed successfully!" || \
(echo "Pipeline failed!" && exit 1)System Maintenance
#!/bin/bash
# Daily maintenance tasks
echo "Starting maintenance..." && \
\
# Update system
apt-get update && apt-get upgrade -y && \
\
# Clean package cache
apt-get autoclean && apt-get autoremove -y && \
\
# Clean old logs
find /var/log -name "*.log" -mtime +30 -delete && \
\
# Update locate database
updatedb && \
\
# Verify disk space
df -h && \
\
echo "Maintenance completed!" || \
echo "Maintenance failed at some step"Application Startup
#!/bin/bash
# Start application with dependencies
echo "Starting services..." && \
\
# Start database
docker-compose up -d postgres && \
sleep 5 && \
\
# Start cache
docker-compose up -d redis && \
sleep 2 && \
\
# Run migrations
npm run migrate && \
\
# Start application
npm start || \
(echo "Startup failed, stopping services..." && \
docker-compose down && \
exit 1)File Processing Pipeline
#!/bin/bash
INPUT="data.csv"
OUTPUT="processed.json"
# Process data through multiple stages
cat "$INPUT" | \
grep -v "^#" | \
sort -u | \
awk -F',' '{print $1, $3}' | \
python transform.py | \
jq '.' > "$OUTPUT" && \
echo "Processing complete: $OUTPUT" || \
echo "Processing failed"Error Handling Patterns
Try-Catch Style
#!/bin/bash
# Simulate try-catch
{
command1 && \
command2 && \
command3
} || {
echo "Error occurred"
# Cleanup or recovery
cleanup_function
exit 1
}Rollback on Failure
#!/bin/bash
# Deploy with automatic rollback
CURRENT_VERSION=$(git rev-parse HEAD)
git pull origin main && \
npm install && \
npm run build && \
pm2 restart app || \
(echo "Deployment failed, rolling back..." && \
git reset --hard $CURRENT_VERSION && \
npm install && \
npm run build && \
pm2 restart app && \
echo "Rolled back to previous version")Notification on Failure
#!/bin/bash
# Send notification if backup fails
./backup.sh && \
echo "Backup successful" || \
(echo "Backup failed!" && \
mail -s "Backup Failed" admin@example.com <<< "Backup job failed at $(date)" && \
curl -X POST https://slack.com/api/chat.postMessage \
-d "text=Backup failed on $(hostname)")One-Liners with Chaining
# Quick directory navigation
mkdir -p ~/projects/newapp && cd ~/projects/newapp && git init
# Download and extract
wget https://example.com/file.tar.gz && tar xzf file.tar.gz && cd extracted-dir
# Build and deploy
make clean && make && make test && make install
# Service restart with verification
systemctl restart nginx && systemctl status nginx || echo "Restart failed"
# Conditional package install
which docker || (curl -fsSL https://get.docker.com | sh && usermod -aG docker $USER)
# Quick backup
tar czf backup-$(date +%Y%m%d).tar.gz /data && echo "Backup created" || echo "Backup failed"Grouping Commands
Subshells with ()
# Run in subshell (doesn't affect current shell)
(cd /tmp && rm -rf *)
# Current directory unchanged
pwd # Still in original directory
# Multiple commands in subshell
(export VAR=value && ./script.sh)
echo $VAR # Empty, VAR not set in parent shellCommand Grouping with {}
# Run in current shell
{ cd /tmp && rm -rf * ; }
# Current directory changed
pwd # Now in /tmp
# Note: spaces and semicolon are required
{ command1 ; command2 ; }Performance Tips
- Use && for dependencies: Stop on first failure
- Use ; for independent tasks: Run all regardless
- Group related operations: Use () or {} for clarity
- Avoid nested chains: Makes debugging harder
- Use functions for complex logic: More readable than long chains
Debugging Chains
# Add set -x to see what's executing
set -x
command1 && command2 && command3
set +x
# Add echo statements between commands
command1 && echo "Step 1 done" && \
command2 && echo "Step 2 done" && \
command3 && echo "Step 3 done"
# Check exit codes explicitly
command1
echo "Exit code: $?"Common Patterns
# Create and enter directory
mkdir -p dir && cd dir
# Download and install
wget URL && tar xzf file.tar.gz && ./install.sh
# Test and deploy
npm test && npm run build && ./deploy.sh
# Backup before change
cp file.txt file.txt.bak && vim file.txt
# Verify before delete
ls file.txt && rm file.txt || echo "File doesn't exist"
# Clone and setup
git clone URL repo && cd repo && npm install
# Check service and restart if needed
systemctl is-active service || systemctl restart serviceComplete Example: Web Server Setup
#!/bin/bash
echo "Setting up web server..." && \
\
# Update system
apt-get update && \
apt-get upgrade -y && \
\
# Install packages
apt-get install -y nginx nodejs npm postgresql && \
\
# Configure firewall
ufw allow 22/tcp && \
ufw allow 80/tcp && \
ufw allow 443/tcp && \
ufw --force enable && \
\
# Setup application directory
mkdir -p /var/www/app && \
cd /var/www/app && \
\
# Clone repository
git clone https://github.com/user/app.git . && \
\
# Install dependencies
npm install && \
\
# Build application
npm run build && \
\
# Setup database
sudo -u postgres psql << EOF && \
CREATE DATABASE appdb;
CREATE USER appuser WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser;
EOF
\
# Configure nginx
cat > /etc/nginx/sites-available/app << 'NGINX' && \
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
}
}
NGINX
\
# Enable site
ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/ && \
nginx -t && \
systemctl restart nginx && \
\
# Start application
npm start && \
\
echo "Web server setup complete!" || \
(echo "Setup failed!" && exit 1)Pro Tips
- Use && for safety: Prevents cascading failures
- Add echo statements: Track progress in long chains
- Use set -e: Exit script on any error
- Break long chains: Use functions for readability
- Test incrementally: Build chains step by step
Common Mistakes
# Wrong - mixing && and ||
command1 && command2 || command3 # Confusing logic
# Wrong - no error handling
command1 ; command2 ; command3 # Continues even if failed
# Wrong - too complex
cmd1 && (cmd2 || cmd3) && (cmd4 || (cmd5 && cmd6)) # Hard to debug
# Better - use functions
deploy() {
cmd1 && cmd2 && cmd3
}
deploy || handle_errorConclusion
Command chaining with &&, ||, and ; is a fundamental skill for writing clean, efficient shell scripts. It eliminates nested if statements, makes error handling automatic, and keeps your code readable. Master these operators and your scripts will be more elegant and maintainable.
Start chaining commands today and watch your shell scripts transform from verbose if-statement forests to clean, elegant pipelines!
