Shell Scripting Essentials

intermediate bash scripting shell automation

Shell scripting lets us automate repetitive tasks. Instead of typing 10 commands every time we deploy, we write a script once and run it forever. Every DevOps engineer writes bash scripts regularly.

The Shebang and Basics

Every script starts with a shebang — it tells the system which interpreter to use.

#!/bin/bash
# This is a comment
echo "Hello from the script!"

Make it executable and run it:

chmod +x deploy.sh
./deploy.sh

Variables

No spaces around =. That’s the #1 mistake beginners make.

#!/bin/bash
name="Manish"
port=3000
echo "Starting $name on port $port"
echo "Home directory is $HOME"   # environment variables work too

# Command substitution — capture a command's output
today=$(date +%Y-%m-%d)
echo "Today is $today"

Use "$variable" (with quotes) to avoid word-splitting issues. This is especially important when variables might contain spaces.

Conditionals

Bash uses if / then / elif / else / fi. The [[ ]] syntax is the modern way to write conditions.

#!/bin/bash
file="/etc/nginx/nginx.conf"

if [[ -f "$file" ]]; then
    echo "Config exists"
elif [[ -d "$file" ]]; then
    echo "It's a directory, not a file"
else
    echo "Config not found!"
    exit 1
fi

Common test flags:

  • -f file — file exists and is a regular file
  • -d dir — directory exists
  • -z "$var" — string is empty
  • -n "$var" — string is not empty
  • $a -eq $b — numeric equality
  • "$a" == "$b" — string equality

Loops

#!/bin/bash
# For loop — iterate over a list
for server in web1 web2 web3; do
    echo "Deploying to $server..."
    # ssh "$server" "cd /app && git pull"
done

# Loop over files
for file in /var/log/*.log; do
    echo "Processing $file"
done

# While loop — read a file line by line
while IFS= read -r line; do
    echo "Line: $line"
done < servers.txt

# C-style for loop
for ((i=1; i<=5; i++)); do
    echo "Attempt $i"
done

Functions

Functions help us organize scripts and avoid repeating code.

#!/bin/bash
log() {
    echo "[$(date '+%H:%M:%S')] $1"
}

check_service() {
    local service=$1   # local keeps the variable scoped to the function
    if systemctl is-active --quiet "$service"; then
        log "$service is running"
        return 0
    else
        log "$service is DOWN!"
        return 1
    fi
}

check_service "nginx"
check_service "postgresql"

Exit Codes and Error Handling

Every command returns an exit code. 0 means success, anything else means failure. We access it with $?.

#!/bin/bash
set -e          # exit immediately if any command fails
set -o pipefail # catch errors in pipes too
set -u          # treat unset variables as errors

# Combined — the holy trinity of safe scripts:
set -euo pipefail

# Check exit codes manually
if ! docker build -t myapp .; then
    echo "Build failed!"
    exit 1
fi

Argument Parsing

Scripts can accept arguments just like regular commands.

#!/bin/bash
# Usage: ./deploy.sh staging v1.2.3

env=$1          # first argument
version=$2      # second argument

echo "Deploying $version to $env"
echo "Total arguments: $#"
echo "All arguments: $@"

# Safety check
if [[ $# -lt 2 ]]; then
    echo "Usage: $0 <environment> <version>"
    exit 1
fi

Practical Example: Health Check Script

Here’s a real-world script that combines everything.

#!/bin/bash
set -euo pipefail

SERVICES=("nginx" "postgresql" "redis")
LOG_FILE="/var/log/health-check.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

for svc in "${SERVICES[@]}"; do
    if systemctl is-active --quiet "$svc"; then
        log "OK: $svc is running"
    else
        log "ALERT: $svc is down — attempting restart"
        systemctl restart "$svc"
    fi
done

In simple language, a bash script is just a text file full of commands we’d normally type by hand. Add some ifs and fors, and we’ve got automation.