Shell Script Hack #11: Command Chaining – Clean Error Handling with && and ||

Shell Script Hack #11: Command Chaining – Clean Error Handling with && and ||

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
fi

Nested 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 app

One 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 -y

OR 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 1

Semicolon (;)

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 1

Database 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) && command4

Multi-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 -d

Real-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 shell

Command 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 service

Complete 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_error

Conclusion

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!

References

Written by:

472 Posts

View All Posts
Follow Me :