การเขียน Shell Script สไตล์ Defensive Programming คือการตั้งสมมติฐานไว้ล่วงหน้าว่า ทุกอินพุตสามารถผิดพลาดได้ ทุกคำสั่งมีโอกาสล้มเหลว และสภาพแวดล้อมภายนอกไม่เสถียร เป้าหมายหลักคือการหยุดการทำงานทันทีก่อนที่ข้อผิดพลาดจะลุกลามจนสร้างความเสียหายต่อระบบ (Fail-Fast and Safe)
1. ล็อกโหมดความปลอดภัยที่หัวไฟล์เสมอ (Strict Flags)
บรรทัดแรกหลัง Shebang ควรเปิดการทำงานของแฟล็กความปลอดภัย
#!/bin/sh
set -eu
set -e(Exit on Error)
หยุดสคริปต์ทันทีเมื่อคำสั่งใดคำสั่งหนึ่งคืนค่า Exit code ที่ไม่ใช่0set -u(No Unset)
แจ้ง Error และหยุดทันทีหากมีการเรียกใช้ตัวแปรที่ยังไม่ได้กำหนดค่า ป้องกันอุบัติเหตุrm -rf "$TARGET/"เมื่อ$TARGETว่างเปล่า
หากใช้ Bash: แนะนำให้ใช้
set -euo pipefailเพิ่มเติม
-o pipefail
ถ้าคำสั่งใดในไปป์ (cmd1 | cmd2 | cmd3) พัง ตัวสคริปต์จะมองว่าทั้งไปป์นั้นล้มเหลวทันที (ปกติจะดูแค่ exit code ของคำสั่งสุดท้าย)
2. กำหนดค่าตัวแปรอย่างรัดกุม (Safe Parameter Expansion)
อย่าอ้างถึงตัวแปรลอย ๆ ให้ใช้กลไก Parameter Expansion ควบคุมค่าเริ่มต้นและตรวจความถูกต้อง
# 1. บังคับว่าต้องมีค่า (Mandatory) หากไม่มีจะหยุดการทำงานพร้อมพิมพ์ error
target_dir="${1:?Error: Target directory parameter is required.}"
# 2. กำหนดค่าเริ่มต้น (Default value) หากไม่มีค่า ให้ใช้ค่าสำรอง
backup_retention="${BACKUP_DAYS:-7}"
# 3. กำหนดค่าและเซ็ตกลับลงตัวแปรหากยังไม่มีค่า (Assign default)
: "${CONFIG_FILE:=/etc/app/config.json}"
3. เทคนิคป้องกันหายนะคำสั่ง rm -rf
คำสั่งลบไฟล์เป็นจุดที่อันตรายที่สุด ให้ตรวจสอบอย่างน้อย 2 ชั้นก่อนลบเสมอ
# อันตรายมาก (ห้ามเขียนเด็ดขาด):
# rm -rf "$cache_dir/*" <-- หาก cache_dir ว่างเปล่า จะกลายเป็น rm -rf /*
# วิธีปลอดภัยระดับ Defensive:
target_cache="${CACHE_DIR:?Error: CACHE_DIR is unset}"
# ตรวจสอบว่าเป็น Directory และไม่ใช่ Root path ก่อนลบ
if [ -d "$target_cache" ] && [ "$target_cache" != "/" ]; then
rm -rf -- "${target_cache:?}"/*
else
printf "Error: Target directory '%s' is invalid or not found.\n" "$target_cache" >&2
exit 1
fi
- เครื่องหมาย
--ป้องกันไม่ให้ชื่อไฟล์ที่ขึ้นต้นด้วยขีด (-rf,-i) ถูกมองเป็นแฟล็กของคำสั่ง
4. จัดการ Clean up ทรัพยากรด้วย trap
ไม่ว่าสคริปต์จะทำงานจบตามปกติ เจอ Error (set -e) หรือถูกผู้ใช้กดยกเลิก (Ctrl+C), ทรัพยากรชั่วคราว (Temp files, Locks) ต้องถูกเคลียร์ทิ้งเสมอ
# สร้าง Temp directory แบบปลอดภัย
tmp_dir="$(mktemp -d)"
cleanup() {
exit_code=$?
# ลบไดเรกทอรีชั่วคราว
rm -rf -- "$tmp_dir"
printf "Cleaned up temporary files.\n" >&2
exit "$exit_code"
}
# ดักจับสัญญาณ EXIT (จบงาน), INT (SIGINT / Ctrl+C), TERM (SIGTERM / Kill)
trap cleanup EXIT INT TERM
5. ควบคุมสภาพแวดล้อม (Environment Isolation)
1. กำหนด PATH ให้ชัดเจน ป้องกัน Path Hijacking
# ล็อก PATH ให้ค้นหาในไดเรกทอรีมาตรฐานก่อนเสมอ
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH
2. ฟิกซ์ Locale ป้องกันพฤติกรรมการ Sort หรือ Regex เพี้ยน
# บังคับใช้ ASCII/C Standard เพื่อให้ผลลัพธ์ของ sort, grep, sed เหมือนกันทุกเซิร์ฟเวอร์
export LC_ALL=C
3. รีเซ็ต IFS ป้องกันการแตกคำเพี้ยน
# คืนค่า Internal Field Separator เป็น Space, Tab, Newline มาตรฐาน
IFS='
'
6. ตรวจสอบการพึ่งพา (Dependency Check) ก่อนเริ่มงาน
ก่อนเรียกใช้เครื่องมือภายนอก ให้ตรวจล่วงหน้าตั้งแต่บรรทัดแรกๆ ของสคริปต์
require_cmd() {
for cmd in "$@"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
printf "Fatal: Required command '%s' not found on system.\n" "$cmd" >&2
exit 127
fi
done
}
# ตรวจสอบว่ามีคำสั่งเหล่านี้ครบถ้วน
require_cmd jq curl rsync
7. ป้องกันการรันซ้อน (Concurrency & Atomic Lock)
หากเป็นสคริปต์ Backup หรือ Cron job ที่ห้ามรันซ้อนกัน ให้ใช้ไดเรกทอรีเป็นตัวล็อก (เนื่องจาก mkdir ในระดับ POSIX เป็น Atomic Operation)
LOCK_DIR="/tmp/my_app_backup.lock"
acquire_lock() {
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
printf "Another instance is already running. Exiting.\n" >&2
exit 1
fi
}
release_lock() {
rmdir "$LOCK_DIR" 2>/dev/null || true
}
acquire_lock
trap release_lock EXIT INT TERM
8. ส่ง Error เข้า Stderr และใช้ Return Codes ให้สื่อความหมาย
- ข้อความแจ้งเตือนหรือข้อผิดพลาดทั้งหมดต้องส่งออกทาง Standard Error (
>&2) เสมอ เพื่อไม่ให้ไปปนเปื้อนข้อมูล Output ของคำสั่งที่ผู้ใช้นำไป Pipe ต่อ - กำหนดรหัส Exit Status ให้ชัดเจน
log_err() {
printf "[ERROR] %s\n" "$*" >&2
}
log_info() {
printf "[INFO] %s\n" "$*" >&2
}
if [ ! -r "$config_file" ]; then
log_err "Config file '$config_file' is missing or unreadable."
exit 2 # 2 = Configuration / Parameter error
fi
สรุป Checklist สำหรับ Defensive Script
| รายการตรวจสอบ | แนวทางปฏิบัติ |
| Safety Flags | ใช้ set -eu (และ -o pipefail บน Bash) |
| Variable References | ครอบ Double Quotes "$var" ทุกครั้ง และใช้ "${VAR:?error}" |
| Command Substitution | ใช้ $(cmd) เสมอ เลิกใช้ Backticks `cmd` |
| Resource Cleanup | ผูก trap เข้ากับ EXIT INT TERM เพื่อล้าง Temp/Lock |
| Safety Double Dash | ใส่ -- หน้าชื่อไฟล์ทุกครั้ง เช่น rm -f -- "$file" หรือ cat -- "$file" |
| Dependencies | เช็ค command -v ล่วงหน้าก่อนเริ่มกระบวนการหลัก |
| Outputs | แยก Logs ไปยัง >&2 และส่งเฉพาะ Data ออก stdout |