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.