Home > Blog > tech

Shell Scripting คืออะไร? สอนเขียน Bash Script สำหรับ DevOps และ SysAdmin 2026

shell scripting bash guide
Shell Scripting Bash Guide 2026
2026-04-08 | tech | 3700 words

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 ไหนดี?

คุณสมบัติBashZshFish
Default บน Linuxใช่ (ส่วนใหญ่)ไม่ไม่
Default บน macOSไม่ (ก่อน Catalina)ใช่ (Catalina+)ไม่
POSIX Complianceสูงสูงต่ำ
Scripting Portabilityดีมากดีจำกัด
Auto-completionพื้นฐานดีมาก (Oh My Zsh)ดีเยี่ยม (Built-in)
Syntax Highlightingไม่มี (ต้อง Plugin)PluginBuilt-in
การใช้ใน CI/CDมาตรฐานน้อยน้อยมาก
คำแนะนำ: ใช้ Bash สำหรับ Scripting เสมอ เพราะเป็นมาตรฐานที่ทำงานได้ทุก Server ส่วน Zsh หรือ Fish ใช้เป็น Interactive Shell บนเครื่องส่วนตัวเพื่อ Productivity ที่ดีกว่า ไม่ควรเขียน Script ด้วย Fish syntax เพราะ Portability ต่ำมาก

เริ่มต้น 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
กฎทอง: ทุก Production Script ต้องเริ่มด้วย 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 ไปอีกนานหลายปี


Back to Blog | iCafe Forex | SiamLanCard | Siam2R