Skip to main content

Shell Scripting


Tại sao cần Shell Scripting?

Shell scripting giúp tự động hoá các tác vụ lặp đi lặp lại: deploy, backup, health check, cleanup... Thay vì gõ 20 lệnh mỗi lần, bạn chạy 1 script.


Cấu trúc Script cơ bản

#!/bin/bash
# ↑ Shebang — báo OS dùng /bin/bash để chạy file này

# Chế độ an toàn (khuyến nghị luôn bật)
set -euo pipefail
# -e : dừng ngay nếu lệnh nào thất bại (exit code != 0)
# -u : dừng nếu dùng biến chưa khai báo (undefined variable)
# -o pipefail : pipe fail nếu bất kỳ lệnh nào trong pipe thất bại

# Comment giải thích
# Tên script: deploy.sh
# Mục đích: Deploy ứng dụng lên server
# Author: sontn
# Date: 2024-01-15
# Chạy script
chmod +x script.sh # cấp quyền thực thi (1 lần)
./script.sh # chạy
bash script.sh # chạy không cần chmod

Variables

# Khai báo biến (không có khoảng trắng hai bên dấu =)
NAME="DevOps"
PORT=8080
IS_PRODUCTION=true

# Dùng biến
echo "Hello $NAME"
echo "Port: ${PORT}" # {} để tránh ambiguity
echo "Port+1: $((PORT + 1))" # arithmetic

# Command substitution — lấy output của lệnh
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
CURRENT_DIR=$(pwd)
IP=$(curl -s https://ifconfig.me)
DISK_USAGE=$(df -h / | awk 'NR==2{print $5}')

echo "Backup at: $TIMESTAMP"
echo "My IP: $IP"
echo "Disk: $DISK_USAGE"

# Biến đặc biệt
echo "$0" # tên script
echo "$1" # argument đầu tiên
echo "$2" # argument thứ 2
echo "$@" # tất cả arguments
echo "$#" # số lượng arguments
echo "$?" # exit code của lệnh trước
echo "$$" # PID của script hiện tại

Arguments và Default values

#!/bin/bash
# Dùng: ./deploy.sh production v1.2.3

ENV=${1:-"staging"} # nếu $1 rỗng → dùng "staging"
VERSION=${2:-"latest"}

echo "Deploying $VERSION to $ENV"

# Kiểm tra argument bắt buộc
if [ $# -lt 2 ]; then
echo "Usage: $0 <environment> <version>"
echo "Example: $0 production v1.2.3"
exit 1
fi

Điều kiện (if / else)

# Cú pháp
if [ condition ]; then
# ...
elif [ condition2 ]; then
# ...
else
# ...
fi

# === Kiểm tra file và thư mục ===
if [ -f "/etc/nginx/nginx.conf" ]; then echo "file tồn tại"; fi
if [ -d "/var/log/nginx" ]; then echo "thư mục tồn tại"; fi
if [ -e "/path" ]; then echo "tồn tại (file hoặc dir)"; fi
if [ -s "file.txt" ]; then echo "file không rỗng"; fi
if [ -r "file.txt" ]; then echo "có quyền đọc"; fi
if [ -x "script.sh" ]; then echo "có quyền thực thi"; fi
if [ ! -f "file.txt" ]; then echo "file KHÔNG tồn tại"; fi

# === So sánh số ===
if [ $PORT -eq 80 ]; then echo "HTTP"; fi
if [ $PORT -ne 443 ]; then echo "không phải HTTPS"; fi
if [ $COUNT -lt 10 ]; then echo "ít hơn 10"; fi
if [ $COUNT -gt 100 ]; then echo "nhiều hơn 100"; fi
if [ $COUNT -le 5 ]; then echo "không quá 5"; fi
if [ $COUNT -ge 1 ]; then echo "ít nhất 1"; fi

# === So sánh chuỗi ===
if [ "$ENV" = "production" ]; then echo "production!"; fi
if [ "$ENV" != "staging" ]; then echo "không phải staging"; fi
if [ -z "$VAR" ]; then echo "chuỗi rỗng"; fi
if [ -n "$VAR" ]; then echo "chuỗi không rỗng"; fi

# === [[ ]] — mạnh hơn [ ] ===
if [[ "$ENV" == "prod"* ]]; then echo "bắt đầu bằng prod"; fi
if [[ "$FILE" =~ \.log$ ]]; then echo "file .log"; fi
if [[ "$A" == "x" && "$B" == "y" ]]; then echo "cả hai"; fi
if [[ "$A" == "x" || "$B" == "y" ]]; then echo "một trong hai"; fi
# Kiểm tra kết quả lệnh
if systemctl is-active --quiet nginx; then
echo "nginx running"
else
echo "nginx down, starting..."
sudo systemctl start nginx
fi

# One-liner (&& và ||)
[ -d "/opt/app" ] || mkdir -p /opt/app # tạo nếu chưa có
systemctl is-active nginx && echo "OK" || echo "DOWN"

Bảng toán tử test:

Toán tửÝ nghĩa
-f filefile tồn tại và là file thường
-d dirthư mục tồn tại
-e pathtồn tại (file hoặc dir)
-s filefile tồn tại và không rỗng
-x filecó quyền thực thi
-z strchuỗi rỗng
-n strchuỗi không rỗng
n1 -eq n2bằng nhau
n1 -lt n2nhỏ hơn
n1 -gt n2lớn hơn

Vòng lặp (Loops)

for loop

# Lặp qua danh sách
for server in web01 web02 web03; do
echo "Checking $server..."
ping -c 1 "$server" > /dev/null \
&& echo " ✓ $server reachable" \
|| echo " ✗ $server unreachable"
done

# Lặp qua range số
for i in {1..10}; do
echo "Iteration $i"
done

# Lặp qua file trong thư mục
for file in /var/log/*.log; do
size=$(du -sh "$file" | cut -f1)
echo "$file: $size"
done

# Lặp qua output của lệnh
for pid in $(pgrep nginx); do
echo "nginx PID: $pid"
done

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

while loop

# Lặp theo điều kiện
count=0
while [ $count -lt 5 ]; do
echo "Count: $count"
((count++))
done

# Đọc từng dòng của file
while IFS= read -r line; do
echo "Processing: $line"
done < /etc/hosts

# Đọc từng dòng của output lệnh
while IFS= read -r server; do
ssh "$server" "uptime"
done < servers.txt

# Retry loop — thử lại cho đến khi thành công
MAX_RETRIES=5
retry=0
while ! curl -sf https://api.example.com/health > /dev/null; do
retry=$((retry + 1))
if [ $retry -ge $MAX_RETRIES ]; then
echo "Failed after $MAX_RETRIES retries"
exit 1
fi
echo "Attempt $retry failed, retrying in 5s..."
sleep 5
done
echo "Service is up!"

Functions

# Khai báo function
function_name() {
local arg1=$1 # local — biến chỉ tồn tại trong function
local arg2=$2
# logic...
return 0 # 0 = success, khác 0 = error
}

# Gọi function
function_name "value1" "value2"
# === Ví dụ thực tế ===

# Logging function với level và timestamp
log() {
local level=$1
shift # bỏ argument đầu (level), $@ giờ là message
local message="$*"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message"
}

# Kiểm tra service
check_service() {
local service=$1
if systemctl is-active --quiet "$service"; then
log "INFO" "$service: running ✓"
return 0
else
log "ERROR" "$service: NOT running ✗"
return 1
fi
}

# Gửi thông báo Slack (ví dụ)
notify_slack() {
local message=$1
local webhook_url=$SLACK_WEBHOOK_URL
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$message\"}" \
"$webhook_url" > /dev/null
}

# Cleanup khi script exit (trap)
cleanup() {
log "INFO" "Cleaning up..."
rm -f /tmp/deploy_lock
}
trap cleanup EXIT # gọi cleanup() khi script kết thúc (kể cả lỗi)

# === Dùng ===
log "INFO" "Script started"
check_service nginx || sudo systemctl start nginx
check_service docker || { log "ERROR" "Docker not running!"; exit 1; }

Script thực tế: Deploy Application

#!/bin/bash
# deploy.sh — Deploy application lên server
# Usage: ./deploy.sh <environment> <version>

set -euo pipefail

# === Cấu hình ===
ENVIRONMENTS=("staging" "production")
APP_DIR="/opt/myapp"
BACKUP_DIR="/opt/backups"
LOG_FILE="/var/log/deploy.log"

# === Màu sắc ===
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# === Functions ===
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $*" | tee -a "$LOG_FILE"; }
ok() { echo -e "${GREEN}[OK]${NC} $*" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "$LOG_FILE"; }
error() { echo -e "${RED}[ERROR]${NC} $*" | tee -a "$LOG_FILE"; exit 1; }

validate_args() {
if [ $# -lt 2 ]; then
error "Usage: $0 <environment> <version>"
fi

local env=$1
local valid=false
for e in "${ENVIRONMENTS[@]}"; do
[ "$e" = "$env" ] && valid=true
done
$valid || error "Invalid environment: $env. Must be: ${ENVIRONMENTS[*]}"
}

backup_current() {
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
log "Creating backup..."
mkdir -p "$BACKUP_DIR"
[ -d "$APP_DIR" ] && cp -r "$APP_DIR" "${BACKUP_DIR}/myapp_${timestamp}"
ok "Backup created: myapp_${timestamp}"
}

deploy() {
local version=$1
log "Deploying version $version..."

# Download release
curl -sf "https://releases.example.com/myapp/${version}.tar.gz" \
-o "/tmp/myapp_${version}.tar.gz" \
|| error "Failed to download version $version"

# Extract
tar -xzf "/tmp/myapp_${version}.tar.gz" -C /tmp/
rsync -av --delete "/tmp/myapp-${version}/" "${APP_DIR}/"
rm -f "/tmp/myapp_${version}.tar.gz"

ok "Version $version deployed to $APP_DIR"
}

restart_service() {
log "Restarting myapp service..."
sudo systemctl restart myapp
sleep 3

if systemctl is-active --quiet myapp; then
ok "Service restarted successfully"
else
error "Service failed to start! Check: journalctl -u myapp -n 50"
fi
}

# === Main ===
validate_args "$@"
ENV=$1
VERSION=$2

log "=== Deploy: $VERSION$ENV ==="

[ "$ENV" = "production" ] && warn "⚠️ DEPLOYING TO PRODUCTION!"

backup_current
deploy "$VERSION"
restart_service

ok "=== Deploy complete: $VERSION on $ENV ==="

Script thực tế: Health Check

#!/bin/bash
# health_check.sh — Kiểm tra sức khoẻ hệ thống

set -euo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'

ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; FAILED=true; }

FAILED=false

echo "=== Health Check: $(hostname)$(date) ==="

# CPU Load
load=$(uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | tr -d ' ')
cores=$(nproc)
threshold=$(echo "$cores * 0.8" | bc)
if (( $(echo "$load < $threshold" | bc -l) )); then
ok "CPU Load: $load (threshold: $threshold)"
else
warn "CPU Load: $load cao (threshold: $threshold)"
fi

# Memory
mem_percent=$(free | awk '/^Mem:/{printf "%.0f", $3/$2*100}')
if [ "$mem_percent" -lt 80 ]; then
ok "Memory: ${mem_percent}% used"
elif [ "$mem_percent" -lt 90 ]; then
warn "Memory: ${mem_percent}% used"
else
fail "Memory: ${mem_percent}% CRITICAL!"
fi

# Disk
disk_percent=$(df / | awk 'NR==2{print $5}' | tr -d '%')
if [ "$disk_percent" -lt 80 ]; then
ok "Disk /: ${disk_percent}% used"
elif [ "$disk_percent" -lt 90 ]; then
warn "Disk /: ${disk_percent}% used"
else
fail "Disk /: ${disk_percent}% CRITICAL!"
fi

# Services
for svc in nginx sshd docker; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
ok "Service $svc: running"
else
fail "Service $svc: NOT running"
fi
done

# HTTP endpoint check
if curl -sf --max-time 5 https://localhost/health > /dev/null 2>&1; then
ok "HTTP health endpoint: OK"
else
fail "HTTP health endpoint: unreachable"
fi

echo "================================"
if [ "$FAILED" = true ]; then
echo -e "${RED}Some checks FAILED!${NC}"
exit 1
else
echo -e "${GREEN}All checks passed.${NC}"
fi

Cron — Chạy Script định kỳ

# Xem và chỉnh sửa crontab
crontab -l # xem crontab của user hiện tại
crontab -e # sửa crontab
sudo crontab -l # crontab của root
sudo crontab -e # sửa crontab của root
# Cú pháp cron:
# ┌───────────── phút (0-59)
# │ ┌───────────── giờ (0-23)
# │ │ ┌───────────── ngày trong tháng (1-31)
# │ │ │ ┌───────────── tháng (1-12)
# │ │ │ │ ┌───────────── ngày trong tuần (0=CN, 1=T2...6=T7)
# │ │ │ │ │
# * * * * * /path/to/command

# Ví dụ:
0 * * * * /opt/scripts/health_check.sh >> /var/log/health.log 2>&1
*/5 * * * * /opt/scripts/monitor.sh # mỗi 5 phút
0 2 * * * /opt/scripts/backup.sh # 2:00 AM mỗi ngày
0 0 * * 0 /opt/scripts/weekly.sh # 0:00 AM mỗi Chủ nhật
0 9 1 * * /opt/scripts/monthly.sh # 9:00 AM ngày 1 hàng tháng
@reboot /opt/scripts/startup.sh # khi reboot
Từ khoá tìm hiểu thêm

awk: xử lý văn bản theo cột (cực kỳ mạnh cho log analysis)

sed: tìm kiếm và thay thế văn bản trong stream

xargs: biến output của lệnh thành arguments

tee: ghi output vừa ra màn hình vừa vào file

trap: xử lý signal trong script (cleanup khi Ctrl+C)

heredoc: viết multi-line string trong script