Shell Scripting Series Part 1 — Variables, Loops, and Functions
You're doing the same 15 commands every deployment — let's turn that into one script. Shell scripting is the automation backbone of Linux. Every DevOps engineer writes shell scripts daily, and mastering the fundamentals will save you hours every week.
Your First Script — The Basics
Every shell script starts with a shebang line that tells the system which interpreter to use.
#!/bin/bash
# my-first-script.sh — A simple script to demonstrate the basics
echo "Hello from $(hostname)!"
echo "Today is $(date '+%Y-%m-%d %H:%M:%S')"
echo "You are logged in as: $(whoami)"
echo "Current directory: $(pwd)"
# Make it executable and run it
chmod +x my-first-script.sh
./my-first-script.sh
Always use #!/bin/bash (not #!/bin/sh) unless you specifically need POSIX compatibility. Bash gives you more features like arrays, [[ ]] tests, and string manipulation.
Variables — Storing and Using Data
Variables in bash have no types — everything is a string unless you explicitly declare otherwise.
#!/bin/bash
# Assigning variables (NO spaces around the = sign!)
name="Goel Academy"
server_count=5
log_dir="/var/log/myapp"
today=$(date '+%Y-%m-%d')
# Using variables — always quote them to handle spaces
echo "Welcome to $name"
echo "Managing $server_count servers"
echo "Logs are in: ${log_dir}"
echo "Date: $today"
# Curly braces are required when variable is next to other text
file="${log_dir}/${today}-app.log"
echo "Log file: $file"
# Read-only variables
readonly APP_VERSION="2.1.0"
# APP_VERSION="3.0.0" # This would cause an error
# Default values — use a default if variable is empty or unset
backup_dir="${BACKUP_DIR:-/tmp/backups}"
echo "Backup directory: $backup_dir"
# String length
message="Hello World"
echo "Length: ${#message}" # Output: 11
# Substring extraction
echo "${message:0:5}" # Output: Hello
echo "${message:6}" # Output: World
Special Variables You Need to Know
| Variable | Meaning |
|---|---|
$0 | Script name |
$1, $2, ... | Positional arguments |
$# | Number of arguments |
$@ | All arguments (as separate words) |
$* | All arguments (as single string) |
$? | Exit code of last command |
$$ | PID of the current script |
$! | PID of last background command |
#!/bin/bash
# args-demo.sh — Understanding script arguments
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Total arguments: $#"
echo "All arguments: $@"
echo "Script PID: $$"
# Usage: ./args-demo.sh hello world
Conditionals — Making Decisions
Bash uses if/elif/else with test conditions inside [[ ]] (the modern way) or [ ].
#!/bin/bash
# health-check.sh — Check if a service is healthy
service_name="${1:-nginx}"
# Check if service is running
if systemctl is-active --quiet "$service_name"; then
echo "[OK] $service_name is running"
else
echo "[FAIL] $service_name is NOT running"
echo "Attempting restart..."
sudo systemctl restart "$service_name"
if systemctl is-active --quiet "$service_name"; then
echo "[RECOVERED] $service_name restarted successfully"
else
echo "[CRITICAL] $service_name failed to restart!"
exit 1
fi
fi
# File and directory checks
config_file="/etc/${service_name}/${service_name}.conf"
if [[ -f "$config_file" ]]; then
echo "[OK] Config file exists: $config_file"
elif [[ -d "/etc/${service_name}" ]]; then
echo "[WARN] Config directory exists but config file missing"
else
echo "[WARN] No config directory found"
fi
# Numeric comparisons
disk_usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ "$disk_usage" -gt 90 ]]; then
echo "[CRITICAL] Disk usage at ${disk_usage}%!"
elif [[ "$disk_usage" -gt 75 ]]; then
echo "[WARNING] Disk usage at ${disk_usage}%"
else
echo "[OK] Disk usage at ${disk_usage}%"
fi
Test Operators Reference
| Operator | Type | Meaning |
|---|---|---|
-f file | File | File exists and is regular file |
-d dir | File | Directory exists |
-r file | File | File is readable |
-w file | File | File is writable |
-x file | File | File is executable |
-s file | File | File exists and is not empty |
-eq, -ne | Numeric | Equal, not equal |
-gt, -lt | Numeric | Greater than, less than |
-ge, -le | Numeric | Greater or equal, less or equal |
==, != | String | String equal, not equal |
-z | String | String is empty |
-n | String | String is not empty |
Loops — Doing Things Repeatedly
For Loops
#!/bin/bash
# Loop over a list of servers
servers=("web-01" "web-02" "db-01" "cache-01")
for server in "${servers[@]}"; do
echo "Checking $server..."
ping -c 1 -W 2 "$server" > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
echo " [OK] $server is reachable"
else
echo " [FAIL] $server is unreachable"
fi
done
# Loop over files
echo ""
echo "=== Log files larger than 100MB ==="
for logfile in /var/log/*.log; do
if [[ -f "$logfile" ]]; then
size=$(stat --format="%s" "$logfile" 2>/dev/null)
if [[ "$size" -gt 104857600 ]]; then
human_size=$(du -h "$logfile" | awk '{print $1}')
echo " $logfile — $human_size"
fi
fi
done
# C-style for loop
echo ""
echo "=== Countdown ==="
for ((i=5; i>=1; i--)); do
echo " $i..."
sleep 1
done
echo " Go!"
While Loops
#!/bin/bash
# wait-for-service.sh — Wait until a service is ready
service_url="${1:-http://localhost:8080/health}"
max_attempts=30
attempt=1
echo "Waiting for $service_url to become available..."
while [[ $attempt -le $max_attempts ]]; do
if curl -s -o /dev/null -w "%{http_code}" "$service_url" | grep -q "200"; then
echo "[OK] Service is ready after $attempt attempt(s)"
exit 0
fi
echo " Attempt $attempt/$max_attempts — not ready yet..."
sleep 2
((attempt++))
done
echo "[FAIL] Service did not become ready after $max_attempts attempts"
exit 1
Functions — Reusable Code Blocks
Functions make your scripts modular and readable.
#!/bin/bash
# deploy.sh — A simple deployment script using functions
# Color output function
log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
log_warn() { echo -e "\033[0;33m[WARN]\033[0m $1"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $1"; }
# Function with return value
check_disk_space() {
local mount_point="${1:-/}"
local threshold="${2:-80}"
local usage
usage=$(df "$mount_point" | awk 'NR==2 {print $5}' | tr -d '%')
if [[ "$usage" -ge "$threshold" ]]; then
log_warn "Disk usage on $mount_point is ${usage}% (threshold: ${threshold}%)"
return 1
else
log_info "Disk usage on $mount_point is ${usage}% — OK"
return 0
fi
}
# Function that validates input
validate_environment() {
local env="$1"
local valid_envs=("dev" "staging" "production")
for valid in "${valid_envs[@]}"; do
if [[ "$env" == "$valid" ]]; then
return 0
fi
done
log_error "Invalid environment: $env"
log_error "Must be one of: ${valid_envs[*]}"
return 1
}
# Main script logic
main() {
local environment="${1:-dev}"
log_info "Starting deployment to: $environment"
# Validate environment
validate_environment "$environment" || exit 1
# Pre-flight checks
check_disk_space "/" 80 || exit 1
log_info "All checks passed — proceeding with deployment"
# ... actual deployment commands here
log_info "Deployment complete!"
}
# Run main function with all script arguments
main "$@"
Arrays — Working with Lists
#!/bin/bash
# Indexed arrays
services=("nginx" "postgresql" "redis" "myapp")
# Add an element
services+=("monitoring")
# Array length
echo "Total services: ${#services[@]}"
# Access by index
echo "First service: ${services[0]}"
echo "Last service: ${services[-1]}"
# Loop through array
for svc in "${services[@]}"; do
status=$(systemctl is-active "$svc" 2>/dev/null || echo "unknown")
echo " $svc: $status"
done
# Associative arrays (dictionaries) — requires bash 4+
declare -A server_roles
server_roles[web-01]="frontend"
server_roles[web-02]="frontend"
server_roles[db-01]="database"
server_roles[cache-01]="cache"
# Loop through associative array
for server in "${!server_roles[@]}"; do
echo " $server -> ${server_roles[$server]}"
done
Reading User Input
#!/bin/bash
# interactive-setup.sh — Example of reading user input
read -p "Enter project name: " project_name
read -p "Enter environment (dev/staging/prod): " environment
read -sp "Enter database password: " db_password
echo "" # New line after hidden input
echo ""
echo "Configuration:"
echo " Project: $project_name"
echo " Environment: $environment"
echo " Password: ****"
read -p "Proceed? (y/N): " confirm
if [[ "${confirm,,}" != "y" ]]; then
echo "Aborted."
exit 0
fi
echo "Setting up $project_name in $environment..."
Putting It Together — A Practical Example
#!/bin/bash
# server-info.sh — Gather system information
get_system_info() {
echo "=== System Information ==="
echo "Hostname: $(hostname)"
echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
echo "Kernel: $(uname -r)"
echo "Uptime: $(uptime -p)"
echo "CPU Cores: $(nproc)"
echo "Total RAM: $(free -h | awk '/Mem:/ {print $2}')"
echo "Disk Usage: $(df -h / | awk 'NR==2 {print $5}')"
echo ""
echo "=== Top 5 Processes by Memory ==="
ps aux --sort=-%mem | head -6
echo ""
echo "=== Network Interfaces ==="
ip -4 addr show | grep -E "inet " | awk '{print $NF, $2}'
echo ""
echo "=== Listening Ports ==="
ss -tlnp | awk 'NR>1 {print $4, $6}' | head -10
}
# Run and optionally save to file
if [[ "$1" == "--save" ]]; then
output_file="/tmp/server-info-$(date +%Y%m%d).txt"
get_system_info > "$output_file"
echo "Saved to $output_file"
else
get_system_info
fi
This is Part 1 of our Shell Scripting series. Next up: Part 2 — Real-World Automation Scripts where we'll build 6 production-ready scripts you can use immediately.
