Shell Scripting เป็นทักษะที่ขาดไม่ได้สำหรับ DevOps Engineer, SysAdmin และ Backend Developer ทุกคน ไม่ว่าจะเป็นการ Automate การ Deploy, จัดการ Log, สร้าง Backup Script, Monitor Server หรือแม้แต่ Provisioning เครื่องใหม่ ทุกอย่างเริ่มต้นจาก Shell Script ในปี 2026 แม้จะมีเครื่องมือ Automation มากมาย แต่ Bash Script ยังคงเป็นพื้นฐานที่ทุกคนต้องเข้าใจ เพราะมันอยู่ทุกที่ ทุก Linux Server และ macOS ติดตั้ง Bash มาให้เสมอ
บทความนี้จะสอน Shell Scripting ตั้งแต่พื้นฐาน ครอบคลุม Variables, Conditionals, Loops, Functions, Error Handling, Regular Expressions ไปจนถึง Practical Scripts ที่ใช้งานจริงในสาย DevOps และ SysAdmin
Shell Scripting คืออะไร?
Shell คือโปรแกรมที่ทำหน้าที่เป็นตัวกลางระหว่างผู้ใช้กับ Operating System Kernel โดยรับคำสั่งจากผู้ใช้ แปลงเป็นคำสั่งที่ Kernel เข้าใจ แล้วแสดงผลลัพธ์กลับมา Shell Script คือไฟล์ที่รวบรวมคำสั่ง Shell หลายคำสั่งเข้าด้วยกัน เพื่อทำงานซ้ำๆ โดยอัตโนมัติ แทนที่จะพิมพ์คำสั่งทีละบรรทัด เขียน Script ครั้งเดียวแล้วรันซ้ำได้ตลอด
Shell Scripting เปรียบเหมือนการเขียนสูตรอาหาร คุณเขียนขั้นตอนทั้งหมดไว้ในไฟล์เดียว แล้วคนอื่นสามารถทำตามขั้นตอนเดียวกันได้ทุกครั้ง ไม่ต้องจำ ไม่ต้องพิมพ์ใหม่ ลดความผิดพลาดจากคน (Human Error) และประหยัดเวลาอย่างมาก
Bash vs Zsh vs Fish — เลือก Shell ไหนดี?
| คุณสมบัติ | Bash | Zsh | Fish |
|---|---|---|---|
| Default บน Linux | ใช่ (ส่วนใหญ่) | ไม่ | ไม่ |
| Default บน macOS | ไม่ (ก่อน Catalina) | ใช่ (Catalina+) | ไม่ |
| POSIX Compliance | สูง | สูง | ต่ำ |
| Scripting Portability | ดีมาก | ดี | จำกัด |
| Auto-completion | พื้นฐาน | ดีมาก (Oh My Zsh) | ดีเยี่ยม (Built-in) |
| Syntax Highlighting | ไม่มี (ต้อง Plugin) | Plugin | Built-in |
| การใช้ใน CI/CD | มาตรฐาน | น้อย | น้อยมาก |
เริ่มต้น Bash Script
Shebang Line
บรรทัดแรกของทุก Script ต้องเป็น Shebang Line ซึ่งบอก OS ว่าจะใช้ Interpreter ตัวไหนรัน Script นี้ Shebang สำคัญมากเพราะถ้าไม่ใส่ OS อาจใช้ Shell ผิดตัวในการรัน Script
#!/bin/bash
# หรือ
#!/usr/bin/env bash # แนะนำ — portable กว่า (หา bash จาก PATH)
# สร้างไฟล์
touch myscript.sh
# ทำให้รันได้
chmod +x myscript.sh
# รัน
./myscript.sh
# หรือ
bash myscript.sh
Script แรก
#!/usr/bin/env bash
# แสดงข้อความ
echo "Hello, World!"
# แสดงวันที่
echo "Today is: $(date '+%Y-%m-%d %H:%M:%S')"
# แสดงข้อมูล System
echo "Hostname: $(hostname)"
echo "User: $(whoami)"
echo "OS: $(uname -s)"
echo "Uptime: $(uptime -p)"
Variables และ Environment Variables
ตัวแปรใน Bash ไม่ต้องประกาศ Type ไม่ต้องใส่ keyword เช่น var หรือ let แค่กำหนดค่าได้เลย แต่มีกฎสำคัญคือ ห้ามมีช่องว่างรอบเครื่องหมาย = ถ้ามีช่องว่าง Bash จะตีความว่าเป็นคำสั่ง
#!/usr/bin/env bash
# ตัวแปร (ห้ามมีช่องว่างรอบ =)
NAME="SiamCafe"
AGE=10
GREETING="Hello, $NAME"
# อ่านค่าตัวแปร (ใส่ $)
echo $NAME
echo "$GREETING"
echo "Age is: $AGE"
# เครื่องหมาย {} สำหรับแยก Variable name ชัดเจน
FILE="report"
echo "${FILE}_2026.txt" # report_2026.txt
# Read-only Variable
readonly PI=3.14159
# PI=3.14 # Error: PI is readonly
# ลบตัวแปร
unset AGE
# Command Substitution
CURRENT_DATE=$(date +%Y-%m-%d)
FILE_COUNT=$(ls -1 | wc -l)
echo "Files: $FILE_COUNT, Date: $CURRENT_DATE"
# Environment Variables (สืบทอดไปยัง Child Process)
export DB_HOST="localhost"
export DB_PORT=5432
# ดู Environment Variables ทั้งหมด
env | sort
# Special Variables
echo "Script name: $0"
echo "First argument: $1"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Exit code of last command: $?"
echo "Current PID: $$"
echo "Last background PID: $!"
Input/Output Redirection และ Pipes
การเปลี่ยนทิศทางของ Input/Output เป็นพื้นฐานสำคัญของ Shell Scripting ทุกคำสั่งใน Linux มี 3 Channel หลัก คือ stdin (input, fd 0), stdout (output, fd 1) และ stderr (error, fd 2) Redirection ช่วยให้ส่ง Output ไปไฟล์ อ่าน Input จากไฟล์ หรือส่ง Error ไปที่อื่นได้
#!/usr/bin/env bash
# Output Redirection
echo "Hello" > output.txt # เขียนทับ
echo "World" >> output.txt # เขียนต่อท้าย
# Input Redirection
wc -l < input.txt # อ่านจากไฟล์
# Stderr Redirection
command_that_fails 2> error.log # Error ไปไฟล์
command_that_fails 2>/dev/null # ทิ้ง Error
command 2>&1 # Stderr ไป Stdout
command > all_output.log 2>&1 # ทุกอย่างไปไฟล์เดียว
command &> all_output.log # เหมือนกัน (Bash shortcut)
# Pipes (ส่ง stdout ของคำสั่งหนึ่งเป็น stdin ของอีกคำสั่ง)
cat access.log | grep "ERROR" | sort | uniq -c | sort -rn | head -10
# Here Document (Multi-line input)
cat << 'EOF' > config.yaml
database:
host: localhost
port: 5432
name: mydb
EOF
# Here String
grep "error" <<< "This has an error message"
# tee — ส่ง output ไปทั้งหน้าจอและไฟล์
echo "important log" | tee -a logfile.txt
# Process Substitution
diff <(ls dir1) <(ls dir2) # เปรียบเทียบรายชื่อไฟล์ 2 โฟลเดอร์
Conditionals — เงื่อนไข
if/else
#!/usr/bin/env bash
# if/else พื้นฐาน
if [ "$1" = "deploy" ]; then
echo "Starting deployment..."
elif [ "$1" = "rollback" ]; then
echo "Rolling back..."
else
echo "Usage: $0 [deploy|rollback]"
exit 1
fi
# เปรียบเทียบตัวเลข
COUNT=15
if [ "$COUNT" -gt 10 ]; then
echo "Count is greater than 10"
fi
# Operators ตัวเลข:
# -eq equal -ne not equal
# -gt greater than -lt less than
# -ge greater/equal -le less/equal
# เปรียบเทียบ String
if [ -z "$VAR" ]; then echo "VAR is empty"; fi
if [ -n "$VAR" ]; then echo "VAR is not empty"; fi
if [ "$A" = "$B" ]; then echo "Equal"; fi
if [ "$A" != "$B" ]; then echo "Not equal"; fi
# ตรวจสอบไฟล์
if [ -f "/etc/nginx/nginx.conf" ]; then
echo "Nginx config exists"
fi
if [ -d "/var/log" ]; then
echo "Log directory exists"
fi
# File test operators:
# -f file exists and is regular file
# -d directory exists
# -e exists (file or dir)
# -r readable -w writable -x executable
# -s file exists and size > 0
# -L symbolic link
# [[ ]] — Extended test (Bash specific, แนะนำ)
if [[ "$FILE" == *.log ]]; then
echo "It's a log file"
fi
if [[ "$URL" =~ ^https?:// ]]; then
echo "Valid URL"
fi
# Logical operators
if [[ -f "$FILE" && -r "$FILE" ]]; then
echo "File exists and is readable"
fi
if [[ "$ENV" = "prod" || "$ENV" = "staging" ]]; then
echo "Non-dev environment"
fi
case Statement
#!/usr/bin/env bash
case "$1" in
start)
echo "Starting service..."
systemctl start myapp
;;
stop)
echo "Stopping service..."
systemctl stop myapp
;;
restart)
echo "Restarting service..."
systemctl restart myapp
;;
status)
systemctl status myapp
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
Loops — วนซ้ำ
#!/usr/bin/env bash
# for loop — วน List
for server in web1 web2 web3 db1; do
echo "Checking $server..."
ssh "$server" uptime
done
# for loop — วน Range
for i in {1..10}; do
echo "Iteration $i"
done
# for loop — วน Files
for file in /var/log/*.log; do
echo "Processing: $file ($(du -h "$file" | cut -f1))"
done
# C-style for loop
for ((i=0; i<5; i++)); do
echo "Count: $i"
done
# while loop
COUNTER=0
while [ $COUNTER -lt 5 ]; do
echo "Counter: $COUNTER"
COUNTER=$((COUNTER + 1))
done
# while — อ่านไฟล์ทีละบรรทัด
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
# while — อ่านจาก Command output
ps aux | while read -r user pid cpu mem rest; do
if (( $(echo "$cpu > 50" | bc -l) )); then
echo "HIGH CPU: PID=$pid User=$user CPU=$cpu%"
fi
done
# until loop (วนจนกว่าเงื่อนไขจะเป็นจริง)
until curl -s http://localhost:8080/health > /dev/null 2>&1; do
echo "Waiting for service to start..."
sleep 2
done
echo "Service is up!"
# break และ continue
for i in {1..20}; do
[ "$i" -eq 5 ] && continue # ข้าม 5
[ "$i" -eq 15 ] && break # หยุดที่ 15
echo "$i"
done
Functions
Function ช่วยจัดกลุ่ม Code ที่ใช้ซ้ำ ทำให้ Script อ่านง่ายและ Maintain ได้ง่ายขึ้น Function ใน Bash รับ Arguments ผ่าน $1, $2, ... เหมือน Script และสามารถ Return Exit Code ได้ แต่ไม่สามารถ Return String โดยตรง ต้องใช้ echo หรือ Global Variable แทน
#!/usr/bin/env bash
# ประกาศ Function
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" >&2
}
log_info "Starting backup..."
log_error "Disk space low!"
# Function กับ Arguments
deploy() {
local env="$1" # local variable (ไม่รั่วออก)
local version="$2"
if [[ -z "$env" || -z "$version" ]]; then
log_error "Usage: deploy "
return 1 # Return exit code (ไม่ใช่ exit — ไม่จบ Script)
fi
log_info "Deploying version $version to $env"
# ... deployment logic ...
return 0
}
deploy "production" "v2.1.0"
# Function ที่ "Return" ค่า (ผ่าน echo + command substitution)
get_disk_usage() {
df -h / | awk 'NR==2 {print $5}' | tr -d '%'
}
USAGE=$(get_disk_usage)
if [ "$USAGE" -gt 80 ]; then
log_error "Disk usage is $USAGE% — above threshold!"
fi
# Function กับ Array
check_services() {
local services=("$@")
for svc in "${services[@]}"; do
if systemctl is-active --quiet "$svc"; then
log_info "$svc: RUNNING"
else
log_error "$svc: STOPPED"
fi
done
}
check_services nginx postgresql redis
Arrays
#!/usr/bin/env bash
# Indexed Array
SERVERS=("web1" "web2" "web3" "db1" "db2")
echo "First: ${SERVERS[0]}"
echo "All: ${SERVERS[@]}"
echo "Count: ${#SERVERS[@]}"
echo "Indices: ${!SERVERS[@]}"
# เพิ่มสมาชิก
SERVERS+=("cache1")
# ลบสมาชิก
unset 'SERVERS[2]'
# วน Array
for server in "${SERVERS[@]}"; do
echo "Server: $server"
done
# Associative Array (Bash 4+)
declare -A CONFIG
CONFIG[host]="localhost"
CONFIG[port]="5432"
CONFIG[db]="mydb"
CONFIG[user]="admin"
echo "Host: ${CONFIG[host]}"
echo "Port: ${CONFIG[port]}"
for key in "${!CONFIG[@]}"; do
echo "$key = ${CONFIG[$key]}"
done
String Manipulation
#!/usr/bin/env bash
STR="Hello World from Bash"
# ความยาว
echo "${#STR}" # 20
# Substring
echo "${STR:6:5}" # World
# Replace (แรก / ทั้งหมด)
echo "${STR/World/DevOps}" # Hello DevOps from Bash
echo "${STR// /_}" # Hello_World_from_Bash
# ลบ Pattern
FILE="backup_2026-04-08.tar.gz"
echo "${FILE%.tar.gz}" # backup_2026-04-08 (ลบ suffix สั้นสุด)
echo "${FILE%%.*}" # backup_2026-04-08 (ลบ suffix ยาวสุด)
echo "${FILE#*/}" # ลบ prefix สั้นสุด
echo "${FILE##*.}" # gz (ลบ prefix ยาวสุด = get extension)
# Uppercase / Lowercase (Bash 4+)
echo "${STR^^}" # HELLO WORLD FROM BASH
echo "${STR,,}" # hello world from bash
echo "${STR^}" # Hello World from Bash (first char upper)
# Default Value
echo "${UNDEFINED_VAR:-default_value}" # default_value
echo "${UNDEFINED_VAR:=set_if_unset}" # set_if_unset (และตั้งค่าด้วย)
echo "${REQUIRED_VAR:?Error: var is required}" # แสดง Error ถ้าว่าง
Error Handling
Error Handling เป็นสิ่งที่แยก Script ระดับมือสมัครเล่นกับมืออาชีพ Script ที่ไม่มี Error Handling จะทำงานต่อแม้คำสั่งก่อนหน้าล้มเหลว ทำให้เกิดผลลัพธ์ที่ไม่คาดคิด อาจลบข้อมูลผิด Deploy Code เสีย หรือ Backup ไฟล์ว่าง
#!/usr/bin/env bash
# set options สำหรับ Error Handling
set -euo pipefail
# -e exit ทันทีเมื่อคำสั่งล้มเหลว (exit code != 0)
# -u error เมื่อใช้ตัวแปรที่ไม่ได้ตั้งค่า
# -o pipefail pipe ล้มเหลวถ้าคำสั่งใดใน pipe ล้มเหลว
# trap — จับ Signal หรือ Exit
cleanup() {
echo "Cleaning up temp files..."
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT # เรียก cleanup เมื่อ Script จบ (ปกติหรือ Error)
trap cleanup ERR # เรียกเมื่อเกิด Error
trap 'echo "Interrupted!"; exit 1' INT TERM # Ctrl+C หรือ kill
TEMP_DIR=$(mktemp -d)
echo "Working in $TEMP_DIR"
# Error Handling แบบ Manual
if ! cp important_file.txt "$TEMP_DIR/"; then
echo "ERROR: Failed to copy file" >&2
exit 1
fi
# || operator (ทำเมื่อล้มเหลว)
cd /nonexistent || { echo "Cannot cd"; exit 1; }
# && operator (ทำเมื่อสำเร็จ)
mkdir -p /tmp/deploy && echo "Directory created"
# ตรวจ Exit Code
some_command
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "Command failed with exit code: $EXIT_CODE"
fi
set -euo pipefail เสมอ มันจะช่วยจับ Bug ที่อาจทำให้ Script ทำงานผิดพลาดอย่างเงียบๆ ทำให้ Script ของคุณ Fail Fast แทนที่จะ Fail Silently
Regular Expressions (grep, sed, awk)
grep — ค้นหา Pattern
#!/usr/bin/env bash
# grep พื้นฐาน
grep "error" /var/log/syslog # ค้นหาคำว่า error
grep -i "error" /var/log/syslog # Case insensitive
grep -r "TODO" ./src/ # ค้นหาใน Directory
grep -c "error" logfile.txt # นับจำนวนบรรทัดที่เจอ
grep -n "error" logfile.txt # แสดงเลขบรรทัด
grep -v "debug" logfile.txt # แสดงบรรทัดที่ไม่มี debug
# Extended Regex (-E = egrep)
grep -E "error|warning|critical" /var/log/syslog
grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" access.log # IP addresses
grep -oP '(?<=email=)[^ ]+' logfile.txt # Perl regex: extract email
sed — Stream Editor
#!/usr/bin/env bash
# sed — แก้ไขข้อความ
sed 's/old/new/' file.txt # แทนที่ครั้งแรกในแต่ละบรรทัด
sed 's/old/new/g' file.txt # แทนที่ทั้งหมด
sed -i 's/old/new/g' file.txt # แก้ไขไฟล์จริง (in-place)
sed -i.bak 's/old/new/g' file.txt # แก้ไข + สร้าง backup
# ลบบรรทัด
sed '/^#/d' config.txt # ลบ Comment lines
sed '/^$/d' config.txt # ลบบรรทัดว่าง
sed '1,5d' file.txt # ลบบรรทัดที่ 1-5
# เพิ่มบรรทัด
sed '3a\New line after line 3' file.txt
sed '1i\Header line' file.txt
# ตัวอย่างจริง: แก้ Config
sed -i "s/^listen_port=.*/listen_port=8080/" app.conf
sed -i "s|DB_HOST=.*|DB_HOST=$NEW_HOST|" .env
awk — Text Processing
#!/usr/bin/env bash
# awk — ประมวลผลข้อความแบบ Column
awk '{print $1, $3}' file.txt # พิมพ์ Column 1 และ 3
awk -F',' '{print $2}' data.csv # ใช้ comma เป็น delimiter
awk 'NR>=10 && NR<=20' file.txt # พิมพ์บรรทัดที่ 10-20
awk '$3 > 100 {print $1, $3}' data.txt # กรองค่ามากกว่า 100
# คำนวณ
awk '{sum += $1} END {print "Total:", sum}' numbers.txt
awk '{sum += $1; count++} END {print "Average:", sum/count}' numbers.txt
# ตัวอย่างจริง: วิเคราะห์ Access Log
# Top 10 IP ที่เข้ามาบ่อยสุด
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# Response time > 1 วินาที
awk '$NF > 1.0 {print $7, $NF "s"}' access.log | head -20
Cron Jobs — ตั้งเวลา Script
# แก้ไข Crontab
crontab -e
# รูปแบบ: minute hour day month weekday command
# ตัวอย่าง:
# รัน Backup ทุกวัน ตี 3
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
# รัน Health Check ทุก 5 นาที
*/5 * * * * /home/user/scripts/health_check.sh
# รัน Log Rotation ทุกวันจันทร์ ตี 1
0 1 * * 1 /home/user/scripts/rotate_logs.sh
# รัน Report ทุกวันที่ 1 ของเดือน
0 9 1 * * /home/user/scripts/monthly_report.sh
# ดู Crontab ปัจจุบัน
crontab -l
# เทคนิค: ใช้ flock ป้องกัน Script ซ้อนกัน
*/5 * * * * flock -n /tmp/myjob.lock /home/user/scripts/job.sh
Practical Scripts สำหรับ DevOps
Script 1: Log Rotation
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="/var/log/myapp"
MAX_AGE=30 # วัน
MAX_SIZE=100 # MB
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
log_info "Starting log rotation..."
# ลบ Log เก่ากว่า MAX_AGE วัน
find "$LOG_DIR" -name "*.log.gz" -mtime +$MAX_AGE -delete
DELETED=$(find "$LOG_DIR" -name "*.log.gz" -mtime +$MAX_AGE 2>/dev/null | wc -l)
log_info "Deleted $DELETED old log files"
# Compress Log ที่ใหญ่กว่า MAX_SIZE MB
find "$LOG_DIR" -name "*.log" -size +${MAX_SIZE}M | while read -r logfile; do
log_info "Compressing: $logfile"
gzip "$logfile"
done
# Rotate Active Logs
for logfile in "$LOG_DIR"/*.log; do
[ -f "$logfile" ] || continue
mv "$logfile" "$logfile.$(date +%Y%m%d)"
gzip "$logfile.$(date +%Y%m%d)"
done
# ส่ง Signal ให้ App เปิด Log file ใหม่
if [ -f /var/run/myapp.pid ]; then
kill -USR1 "$(cat /var/run/myapp.pid)" 2>/dev/null || true
fi
log_info "Log rotation completed"
Script 2: Backup Database
#!/usr/bin/env bash
set -euo pipefail
DB_NAME="production_db"
DB_USER="backup_user"
BACKUP_DIR="/backups/postgresql"
RETENTION_DAYS=14
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
trap 'log "ERROR: Backup failed!"; rm -f "$BACKUP_FILE"' ERR
log "Starting backup of $DB_NAME..."
# สร้าง Directory
mkdir -p "$BACKUP_DIR"
# Dump + Compress
pg_dump -U "$DB_USER" -d "$DB_NAME" -Fc | gzip > "$BACKUP_FILE"
# ตรวจสอบขนาด
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Backup created: $BACKUP_FILE ($SIZE)"
# ลบ Backup เก่า
DELETED=$(find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "Deleted $DELETED old backups (>$RETENTION_DAYS days)"
# Verify Backup (ทดสอบว่า decompress ได้)
if gzip -t "$BACKUP_FILE" 2>/dev/null; then
log "Backup verification: OK"
else
log "ERROR: Backup file is corrupted!"
exit 1
fi
log "Backup completed successfully"
Script 3: Health Check
#!/usr/bin/env bash
set -euo pipefail
SERVICES=("nginx" "postgresql" "redis-server")
ENDPOINTS=("http://localhost:80" "http://localhost:8080/health")
DISK_THRESHOLD=85
MEM_THRESHOLD=90
ALERT_EMAIL="admin@example.com"
STATUS=0
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
alert() { log "ALERT: $1"; STATUS=1; }
log "=== Health Check Start ==="
# ตรวจ Services
for svc in "${SERVICES[@]}"; do
if systemctl is-active --quiet "$svc"; then
log "Service $svc: OK"
else
alert "Service $svc is DOWN!"
fi
done
# ตรวจ HTTP Endpoints
for url in "${ENDPOINTS[@]}"; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$url" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
log "Endpoint $url: OK ($HTTP_CODE)"
else
alert "Endpoint $url: FAIL ($HTTP_CODE)"
fi
done
# ตรวจ Disk Usage
while read -r usage mount; do
usage_num=${usage%\%}
if [ "$usage_num" -gt "$DISK_THRESHOLD" ]; then
alert "Disk $mount usage: $usage (threshold: $DISK_THRESHOLD%)"
else
log "Disk $mount: $usage"
fi
done < <(df -h --output=pcent,target | tail -n +2 | grep -v tmpfs)
# ตรวจ Memory
MEM_USED=$(free | awk '/Mem/ {printf "%.0f", $3/$2*100}')
if [ "$MEM_USED" -gt "$MEM_THRESHOLD" ]; then
alert "Memory usage: $MEM_USED% (threshold: $MEM_THRESHOLD%)"
else
log "Memory: $MEM_USED%"
fi
# ตรวจ Load Average
LOAD=$(awk '{print $1}' /proc/loadavg)
CORES=$(nproc)
log "Load Average: $LOAD (Cores: $CORES)"
log "=== Health Check End (Status: $STATUS) ==="
exit $STATUS
Script 4: Deployment Script
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="myapp"
APP_DIR="/opt/$APP_NAME"
REPO_URL="git@github.com:company/$APP_NAME.git"
BRANCH="${1:-main}"
ROLLBACK_COUNT=3
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$APP_NAME] $1"; }
trap 'log "ERROR: Deployment failed! Rolling back..."; rollback' ERR
rollback() {
local prev_release
prev_release=$(ls -1t "$APP_DIR/releases/" 2>/dev/null | sed -n '2p')
if [ -n "$prev_release" ]; then
ln -sfn "$APP_DIR/releases/$prev_release" "$APP_DIR/current"
systemctl restart "$APP_NAME"
log "Rolled back to: $prev_release"
else
log "No previous release to rollback to!"
fi
}
RELEASE="$(date +%Y%m%d%H%M%S)"
RELEASE_DIR="$APP_DIR/releases/$RELEASE"
log "Deploying $APP_NAME (branch: $BRANCH, release: $RELEASE)"
# 1. Clone
mkdir -p "$RELEASE_DIR"
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$RELEASE_DIR"
# 2. Install Dependencies
cd "$RELEASE_DIR"
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
elif [ -f "package.json" ]; then
npm ci --production
fi
# 3. Run Migrations
if [ -f "manage.py" ]; then
python manage.py migrate --no-input
fi
# 4. Switch Symlink
ln -sfn "$RELEASE_DIR" "$APP_DIR/current"
# 5. Restart Service
systemctl restart "$APP_NAME"
sleep 3
# 6. Health Check
if curl -sf http://localhost:8080/health > /dev/null; then
log "Deployment successful!"
else
log "Health check failed!"
rollback
exit 1
fi
# 7. Cleanup Old Releases
cd "$APP_DIR/releases"
ls -1t | tail -n +$((ROLLBACK_COUNT + 1)) | xargs -r rm -rf
log "Kept last $ROLLBACK_COUNT releases"
ShellCheck — Linting สำหรับ Shell Script
ShellCheck เป็นเครื่องมือ Static Analysis สำหรับ Shell Script ที่ช่วยหา Bug, Code Smell และปัญหา Portability ก่อนรัน Script จริง ทุกคนที่เขียน Shell Script ควรใช้ ShellCheck เป็นประจำ เพราะ Bash มี Pitfalls มากมายที่ดูถูกต้องแต่ทำงานผิด
# ติดตั้ง ShellCheck
sudo apt install shellcheck # Ubuntu/Debian
brew install shellcheck # macOS
# ใช้งาน
shellcheck myscript.sh
# ตัวอย่าง ShellCheck warnings:
# SC2086: Double quote to prevent word splitting
# SC2046: Quote this to prevent word splitting
# SC2034: Variable appears unused
# SC2154: Variable is referenced but not assigned
# ใน CI/CD Pipeline
shellcheck --severity=warning scripts/*.sh
# Disable specific rule (ใส่ใน Script)
# shellcheck disable=SC2086
echo $VARIABLE
Best Practices สำหรับ Shell Scripting
1. เสมอใส่ Quotes รอบ Variables
# BAD (Word splitting + Globbing)
cp $FILE $DEST
for f in $(ls *.txt); do echo $f; done
# GOOD
cp "$FILE" "$DEST"
for f in *.txt; do echo "$f"; done
2. ใช้ local Variables ใน Functions
my_function() {
local result="something" # ไม่รั่วออกนอก function
echo "$result"
}
3. ใช้ Arrays แทน String ที่มี Spaces
# BAD
FILES="file 1.txt file 2.txt"
# GOOD
FILES=("file 1.txt" "file 2.txt")
for f in "${FILES[@]}"; do echo "$f"; done
4. Portable Script
# ใช้ #!/usr/bin/env bash แทน #!/bin/bash
# ใช้ $(command) แทน `command`
# ใช้ $((math)) แทน expr
# ทดสอบบน Bash 4+ เป็นอย่างน้อย
5. เมื่อไหร่ควรเปลี่ยนจาก Bash ไป Python
# ใช้ Bash เมื่อ:
# - Script สั้นๆ (< 100 บรรทัด)
# - เรียกคำสั่ง CLI เป็นหลัก
# - Text Processing ง่ายๆ
# - Quick automation
# เปลี่ยนไป Python เมื่อ:
# - Script ยาวเกิน 100 บรรทัด
# - ต้องจัดการ JSON/YAML/XML
# - ต้องการ Data Structures ซับซ้อน
# - ต้องการ Error Handling ที่ดี
# - ต้อง Test ได้
# - ต้องการ Cross-platform
Useful One-Liners Collection
# หา 10 ไฟล์ใหญ่สุดใน Directory
find /path -type f -exec du -h {} + 2>/dev/null | sort -rh | head -10
# นับจำนวนไฟล์ในแต่ละ Subdirectory
find . -maxdepth 1 -type d -exec sh -c 'echo "$(find "$1" -type f | wc -l) $1"' _ {} \; | sort -rn
# ดู Process ที่ใช้ Memory สูงสุด
ps aux --sort=-%mem | head -11
# ดู Port ที่เปิดอยู่
ss -tlnp
# หา IP ที่ Request มาบ่อยสุดจาก Access Log
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Monitor Log แบบ Real-time
tail -f /var/log/syslog | grep --line-buffered "error"
# Parallel Execution (ใช้ xargs)
cat servers.txt | xargs -I{} -P 4 ssh {} "uptime"
# แปลง Timestamp
date -d @1680000000
# สร้าง Random Password
openssl rand -base64 32 | head -c 24
# ดูขนาด Directory แยกตาม Subdirectory
du -h --max-depth=1 /path | sort -rh
# Replace String ในหลายไฟล์
find . -name "*.conf" -exec sed -i 's/old_value/new_value/g' {} +
# ตรวจ SSL Certificate Expiry
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
# สร้าง Tar + Compress + Progress
tar cf - /path/to/dir | pv | gzip > backup.tar.gz
# Kill Zombie Processes
ps aux | awk '$8 == "Z" {print $2}' | xargs -r kill -9
# HTTP Request ด้วย curl
curl -s -X POST -H "Content-Type: application/json" -d '{"key":"value"}' http://api.example.com/endpoint | jq .
สรุป
Shell Scripting เป็นทักษะพื้นฐานที่ทรงพลังสำหรับ DevOps Engineer และ SysAdmin ทุกคน การเขียน Bash Script ที่ดีเริ่มจากพื้นฐานที่แข็งแกร่ง ตั้งแต่ Variables, Conditionals, Loops, Functions ไปจนถึง Error Handling ที่เหมาะสม การใส่ set -euo pipefail ทุก Script การใช้ ShellCheck เพื่อตรวจหา Bug และการ Quote Variables เสมอ จะทำให้ Script ของคุณเชื่อถือได้ในระดับ Production
Practical Scripts ที่แสดงในบทความนี้ ไม่ว่าจะเป็น Log Rotation, Database Backup, Health Check หรือ Deployment Script ล้วนเป็น Pattern ที่ใช้งานจริงในทุกองค์กร เริ่มจากการทำความเข้าใจพื้นฐาน แล้วค่อยๆ สร้าง Script Library ของตัวเอง เมื่อ Script ซับซ้อนเกินไป ให้พิจารณาเปลี่ยนไปใช้ Python แทน แต่ Bash Script จะยังคงเป็นเครื่องมือที่ขาดไม่ได้ในสาย DevOps ไปอีกนานหลายปี
