/static/codemoomoo2.png

6. Shell Script in Linux

Shell Script คือเครื่องมือทรงพลังสำหรับการอัตโนมัติงาน (Automation) บนระบบ Linux/Unix โดยใช้ภาษา Shell สั่งงานผ่าน Command Interpreter เพื่อรวมคำสั่งหลาย ๆ คำสั่งเข้าด้วยกันให้ทำงานเป็นโปรแกรมเดียว ตั้งแต่งานง่าย ๆ เช่น สำรองไฟล์ ไปจนถึงระบบ Deployment และ Monitoring ที่ซับซ้อน บทนี้จะกล่าวถึงพื้นฐานการเขียน Shell Script ตั้งแต่โครงสร้างพื้นฐาน ตัวแปร เงื่อนไข วนซ้ำ ฟังก์ชัน ไปจนถึงเทคนิคขั้นสูง การจัดการข้อผิดพลาด และตัวอย่างประยุกต์ใช้งานจริง โดยเน้น Bash ซึ่งเป็น Shell ที่ใช้กันแพร่หลายที่สุด


6.1 พื้นฐาน Shell Scripting (Shell Scripting Fundamentals)

Shell Script คือไฟล์ข้อความ (Text File) ที่เก็บคำสั่ง Shell หลายคำสั่งเรียงต่อกัน เพื่อให้ Shell อ่านและประมวลผลตามลำดับ เปรียบเสมือนการเขียน "โปรแกรม" ด้วยภาษาที่ Shell เข้าใจ ข้อดีหลักของ Shell Script คือสามารถนำคำสั่ง CLI ที่ใช้บ่อย ๆ มารวมกันเป็นชุดคำสั่งอัตโนมัติได้ทันทีโดยไม่ต้องคอมไพล์

flowchart LR
    A[ผู้ใช้ User] -->|พิมพ์คำสั่ง
type command| B[Shell
ตัวแปลคำสั่ง] B -->|Parse + Expand| C{ประเภท
type?} C -->|Builtin| D[ทำงานในตัว Shell
builtin command] C -->|External| E[เรียก fork+exec
spawn process] C -->|Script File| F[อ่านบรรทัดต่อบรรทัด
read line by line] F --> B D --> G[ผลลัพธ์
output] E --> G

Shell แต่ละตัวมีไวยากรณ์และความสามารถแตกต่างกัน การเลือกใช้ Shell ขึ้นอยู่กับ Use Case เช่น สคริปต์ที่ต้องพกพาไปใช้หลายระบบ (Portability) ควรใช้ POSIX sh แต่ถ้าใช้งานส่วนตัวเน้นความสะดวกอาจเลือก zsh หรือ fish

Shell คำอธิบาย ตำแหน่งทั่วไป จุดเด่น จุดด้อย
bash (Bourne Again Shell) Shell มาตรฐานของ Linux ส่วนใหญ่ /bin/bash มี Feature ครบ ใช้แพร่หลาย เอกสารเยอะ ช้ากว่า dash, ไวยากรณ์เก่าซับซ้อน
zsh (Z Shell) ขยายจาก bash เพิ่ม Feature ทันสมัย /bin/zsh Auto-completion ดีเยี่ยม, ใช้งานบน macOS เป็น Default ไม่ได้ติดตั้งมาพร้อม distros ทุกตัว
sh (POSIX Shell) Shell มาตรฐาน POSIX /bin/sh Portable สูงสุด, เร็ว Feature น้อย, ไม่มี array ฯลฯ
dash (Debian Almquist Shell) Implementation ของ POSIX sh ที่เน้นเร็ว /bin/dash เร็ว ขนาดเล็ก เหมาะกับ boot script Feature น้อย ไม่เหมาะ interactive
ksh (Korn Shell) Shell คลาสสิกของ Unix /bin/ksh ทำงานเร็ว เหมาะกับงาน Enterprise ความนิยมลดลง
fish (Friendly Interactive Shell) Shell สำหรับมนุษย์ /usr/bin/fish Auto-suggestion ฉลาด, ไม่ต้องตั้งค่า ไม่ POSIX-compatible (ไวยากรณ์ต่าง)

ตรวจสอบ Shell ที่ใช้อยู่ปัจจุบันและที่ติดตั้งในระบบ:

# ตรวจสอบ Shell ที่กำลังใช้งานอยู่ check current shell
echo "$SHELL"          # ตัวแปร environment ของ login shell
echo "$0"              # process ตัวที่กำลังรัน

# ดู Shell ที่ติดตั้งทั้งหมดในระบบ list available shells
cat /etc/shells

# เปลี่ยน default shell สำหรับ user (ต้อง logout/login ใหม่)
chsh -s /usr/bin/zsh   # เปลี่ยนเป็น zsh

6.1.2 Shebang (Hash-Bang)

Shebang คืออักขระสองตัวแรกของไฟล์สคริปต์ที่ขึ้นต้นด้วย #! ตามด้วย path ของ interpreter ที่จะใช้รันสคริปต์นี้ Kernel จะอ่าน 2 byte แรก (magic number 0x23 0x21) เมื่อเรียกไฟล์เป็น executable แล้วใช้ interpreter ที่ระบุไว้ในการประมวลผล

#!/bin/bash                    # ใช้ bash โดยตรง path คงที่
#!/usr/bin/env bash            # ค้นหา bash จาก PATH (ยืดหยุ่นกว่า portable)
#!/usr/bin/env -S bash -e      # -S อนุญาต option ผ่าน env (Linux เท่านั้น)
#!/bin/sh                      # POSIX shell (สูงสุดในความ portable)
#!/usr/bin/env python3         # ใช้ Python เป็น interpreter ก็ได้

ความแตกต่างระหว่าง #!/bin/bash กับ #!/usr/bin/env bash:

6.1.3 การกำหนดสิทธิ์ Execute (Execute Permission)

ก่อนรันสคริปต์ด้วย ./script.sh ต้องตั้งสิทธิ์ execute ให้กับไฟล์ก่อน เพราะ Linux จะตรวจสอบ permission bit x ก่อนอนุญาตให้รันไฟล์เป็นโปรแกรม

# สร้างสคริปต์ตัวอย่าง create example script
cat > hello.sh <<'EOF'
#!/usr/bin/env bash
echo "สวัสดี $USER, วันนี้วันที่ $(date +%Y-%m-%d)"
EOF

# ตรวจสอบ permission ก่อน check current permission
ls -l hello.sh
# ผลลัพธ์: -rw-r--r-- 1 user user 78 ... hello.sh

# ใส่สิทธิ์ execute ให้เจ้าของไฟล์ add execute permission
chmod +x hello.sh
# หรือเฉพาะ owner เท่านั้น
chmod u+x hello.sh
# หรือใช้ octal
chmod 755 hello.sh

ls -l hello.sh
# ผลลัพธ์: -rwxr-xr-x 1 user user 78 ... hello.sh

# รันสคริปต์
./hello.sh

6.1.4 วิธีการรันสคริปต์ (Methods to Run Scripts)

มี 3 วิธีหลักในการรันสคริปต์ ซึ่งให้ผลลัพธ์ต่างกันด้านการ Spawn Process และการแชร์ Environment Variable

flowchart TD
    P[Parent Shell
เปิดอยู่] --> M{วิธีรัน
method?} M -->|./script.sh| S1[Spawn Subshell
fork + exec] M -->|bash script.sh| S2[Spawn Subshell
ระบุ interpreter] M -->|source script.sh
หรือ . script.sh| S3[รันใน Shell ปัจจุบัน
ไม่ fork] S1 --> R1[Variable ในสคริปต์
หายไปเมื่อจบ] S2 --> R1 S3 --> R2[Variable คงอยู่
ใน Shell หลัก]
# ----- วิธีที่ 1: ./script.sh -----
# ต้องมี shebang และ executable permission
# จะ spawn subshell ใหม่
./hello.sh

# ----- วิธีที่ 2: bash script.sh -----
# ไม่ต้องมี shebang หรือ executable permission
# ระบุ interpreter ตอนเรียก ใช้ทดสอบสคริปต์ได้
bash hello.sh
sh hello.sh        # รันด้วย POSIX sh

# ----- วิธีที่ 3: source script.sh หรือ . script.sh -----
# รันในกระบวนการ shell ปัจจุบัน (ไม่ spawn subshell)
# ใช้สำหรับโหลดตัวแปร, function เข้า shell ปัจจุบัน
source ~/.bashrc
. ~/.bashrc          # ตัวจุด . เทียบเท่า source แต่ POSIX

# ตัวอย่างความแตกต่าง difference example
cat > setvar.sh <<'EOF'
#!/usr/bin/env bash
export MYVAR="Hello"
EOF
chmod +x setvar.sh

./setvar.sh ; echo "$MYVAR"     # ว่างเปล่า: subshell หายไปแล้ว
source setvar.sh ; echo "$MYVAR" # แสดง "Hello": รันใน shell เดิม

6.1.5 Comment และการเขียน Header

Comment ใน Shell Script ขึ้นต้นด้วยเครื่องหมาย # (ยกเว้นบรรทัดแรกที่เป็น Shebang) Shell จะข้ามทุกอย่างหลัง # ในบรรทัดนั้น การเขียน Header ที่ดีช่วยให้ผู้อื่นเข้าใจวัตถุประสงค์ ผู้เขียน วันที่ และวิธีใช้งาน

#!/usr/bin/env bash
#===============================================================================
# ชื่อสคริปต์   : backup_home.sh
# คำอธิบาย     : สำรองข้อมูล home directory ไปยัง external drive
# Author       : อรรถพล คงหวาน <attapon@example.com>
# วันที่สร้าง   : 2026-04-25
# Version      : 1.2.0
# Usage        : ./backup_home.sh [destination]
# Dependencies : tar, gzip, rsync
# Exit Code    : 0=สำเร็จ, 1=Argument ผิด, 2=Destination ไม่พบ
#===============================================================================

# ----- ส่วนของการประกาศตัวแปร global -----
SOURCE_DIR="$HOME"            # ต้นทางที่จะสำรอง source directory
DEST_DIR="${1:-/mnt/backup}"  # ปลายทาง รับจาก arg ตัวที่ 1

# Multi-line comment (ไม่มีในตัว แต่ใช้ : (no-op) + heredoc แทนได้)
: <<'BLOCK_COMMENT'
นี่คือคอมเมนต์หลายบรรทัด
ใช้คำสั่ง : (no-op) ร่วมกับ heredoc
ที่ไม่ทำอะไรเลย ใช้แทน /* ... */ ของภาษา C
BLOCK_COMMENT

# TODO: เพิ่มการตรวจสอบเนื้อที่ disk ก่อนสำรอง
# FIXME: บั๊กเรื่อง symlink ลึกซ้อน

echo "เริ่มต้นสำรอง $SOURCE_DIR -> $DEST_DIR"

ระบบ Tag ที่นิยมในคอมเมนต์: TODO: (สิ่งที่ต้องทำต่อ), FIXME: (จุดที่มีบั๊ก), NOTE: (หมายเหตุสำคัญ), HACK: (วิธีแก้ปัญหาชั่วคราว), XXX: (จุดอันตราย)


6.2 ตัวแปร (Variables)

ตัวแปรใน Shell ใช้เก็บค่าข้อมูลที่จะนำมาใช้ในสคริปต์ ตัวแปรใน Shell เป็น untyped (ไม่มีชนิดข้อมูลตายตัว) ทุกตัวแปรเก็บเป็น string โดยปริยาย แต่สามารถใช้ในบริบท arithmetic เพื่อตีความเป็นตัวเลขได้

6.2.1 การประกาศและเรียกใช้ตัวแปร (Variable Declaration & Reference)

#!/usr/bin/env bash

# ----- การประกาศตัวแปร declaration -----
# กฎสำคัญ: ห้ามมีช่องว่างรอบ = อย่างเด็ดขาด
name="Moo"               # ถูกต้อง correct
age=42                   # ถูกต้อง correct
# name = "Moo"           # ❌ ผิด: shell จะคิดว่า name คือคำสั่ง

# ----- การเรียกใช้ reference -----
echo $name               # Moo
echo "$name"             # Moo (แนะนำ ใส่ double-quote ป้องกัน word splitting)
echo "${name}"           # Moo (รูปแบบเต็ม unambiguous)
echo "${name}_suffix"    # Moo_suffix (ต้องใช้ {} เมื่อต่อกับตัวอักษร)
echo '$name'             # $name (single-quote ไม่ขยายตัวแปร no expansion)

# ----- ตัวแปรที่ยังไม่ได้กำหนดค่า unset variable -----
echo "$undef"            # ว่างเปล่า empty string
echo "${undef:-default}" # default (ถ้าว่าง ใช้ค่า default)
echo "${undef:=default}" # กำหนดค่า default ให้ตัวแปรด้วย assign
echo "${undef:?error}"   # ถ้าว่าง พิมพ์ error และออก exit
echo "${undef:+alt}"     # ถ้าไม่ว่าง คืน alt มิฉะนั้นว่าง

ตารางสรุป Parameter Expansion พื้นฐาน:

รูปแบบ ความหมาย
${var} ค่าของ var (รูปแบบเต็ม)
${var:-default} ใช้ default ถ้า var ว่างหรือไม่ตั้ง
${var:=default} กำหนด default ให้ var เลย ถ้า var ว่าง
${var:?msg} error และ exit ถ้า var ว่าง
${var:+alt} ใช้ alt ถ้า var ไม่ว่าง

6.2.2 Local vs Global Variable

ตัวแปรที่ประกาศนอก function หรือใน global scope จะเป็น global เข้าถึงได้ทุกที่ในสคริปต์ ส่วน local ใช้ในฟังก์ชันเพื่อจำกัด scope ให้อยู่ในฟังก์ชันนั้น ๆ ป้องกันการชนกับตัวแปรชื่อเดียวกันภายนอก

#!/usr/bin/env bash

# ตัวแปร global ใช้ได้ทั้งสคริปต์
counter=0

increment() {
    local counter=10        # ตัวแปร local เฉพาะใน function
    counter=$((counter + 1))
    echo "ภายใน function: $counter"   # 11
}

increment
echo "ภายนอก function: $counter"      # 0 (ไม่ถูกแก้ไข)

# ตัวอย่างที่ไม่มี local อันตราย
without_local() {
    counter=99              # แก้ตัวแปร global โดยไม่ตั้งใจ!
}
without_local
echo "หลังเรียก without_local: $counter"  # 99 (ถูกแก้ไข)

คำแนะนำ: ใช้ local กับตัวแปรในฟังก์ชันเสมอ เพื่อป้องกัน Side Effect ที่ไม่คาดคิด นี่เป็น Best Practice ที่สำคัญมาก

6.2.3 Environment Variables (ตัวแปรสภาพแวดล้อม)

Environment Variable คือตัวแปรที่ส่งต่อจาก Process แม่ไปยัง Process ลูก ใช้คำสั่ง export เพื่อทำให้ตัวแปร global นั้นเข้าถึงได้จาก subshell และโปรแกรมที่เรียกขึ้นมา

# ----- ตัวแปร environment ที่สำคัญ important env variables -----
echo "$HOME"     # /home/moo - home directory ของผู้ใช้
echo "$USER"     # moo - ชื่อผู้ใช้ปัจจุบัน
echo "$PWD"      # /current/dir - working directory ปัจจุบัน
echo "$OLDPWD"   # directory ก่อนหน้า cd
echo "$SHELL"    # /bin/bash - login shell
echo "$PATH"     # /usr/local/bin:/usr/bin:... - paths สำหรับค้นคำสั่ง
echo "$LANG"     # th_TH.UTF-8 - locale ระบบ
echo "$EDITOR"   # /usr/bin/nvim - editor default
echo "$TERM"     # xterm-256color - ประเภท terminal
echo "$HOSTNAME" # ชื่อเครื่อง

# ----- การสร้าง environment variable -----
export MY_API_KEY="sk-1234567890"        # ส่งไปยัง subprocess ได้
MY_LOCAL=just-here                       # เฉพาะ shell ปัจจุบัน

# ดูตัวแปร environment ทั้งหมด list all env vars
env
printenv
printenv HOME       # ดูเฉพาะ HOME

# ลบตัวแปร remove
unset MY_API_KEY

การเพิ่ม path เข้า PATH (รูปแบบที่ปลอดภัย):

# ใส่ path ใหม่ไว้ข้างหน้า (ความสำคัญสูง)
export PATH="$HOME/.local/bin:$PATH"

# ใส่ต่อท้าย (ความสำคัญต่ำ)
export PATH="$PATH:/opt/myapp/bin"

6.2.4 Special Variables (ตัวแปรพิเศษ)

Bash มีตัวแปรพิเศษที่ Shell กำหนดให้อัตโนมัติ ใช้เข้าถึงข้อมูลเกี่ยวกับสคริปต์และกระบวนการ

ตัวแปร ความหมาย
$0 ชื่อสคริปต์ (script name)
$1, $2, ..., $9 argument ตำแหน่งที่ 1-9
${10}, ${11}, ... argument ตำแหน่ง 10 ขึ้นไป (ต้องใช้ {})
$# จำนวน argument ทั้งหมด
$@ argument ทั้งหมด แยกแต่ละตัว "$@" = "$1" "$2" ...
$* argument ทั้งหมด รวมเป็น string เดียว "$*" = "$1 $2 ..."
$? exit code ของคำสั่งล่าสุด (0=สำเร็จ, อื่น=ผิดพลาด)
$$ PID ของ shell ปัจจุบัน
$! PID ของ background job ล่าสุด
$_ argument สุดท้ายของคำสั่งก่อนหน้า
$IFS Internal Field Separator (default = space, tab, newline)
$RANDOM ตัวเลขสุ่ม 0-32767
$LINENO หมายเลขบรรทัดปัจจุบันในสคริปต์
$SECONDS จำนวนวินาทีที่ shell รันมาแล้ว
#!/usr/bin/env bash
# ทดสอบ: ./args.sh apple "banana split" cherry

echo "ชื่อสคริปต์    : $0"          # ./args.sh
echo "จำนวน arg     : $#"           # 3
echo "arg ตัวที่ 1   : $1"           # apple
echo "arg ตัวที่ 2   : $2"           # banana split
echo "arg ตัวที่ 3   : $3"           # cherry
echo "arg ทั้งหมด @ : $@"           # apple banana split cherry
echo "arg ทั้งหมด *  : $*"           # apple banana split cherry
echo "PID           : $$"           # เช่น 12345
echo "บรรทัดนี้     : $LINENO"

# ความแตกต่าง $@ vs $* เมื่อใส่ double quote
for arg in "$@"; do echo "@: [$arg]"; done
# @: [apple]
# @: [banana split]
# @: [cherry]

for arg in "$*"; do echo "*: [$arg]"; done
# *: [apple banana split cherry]   ← รวมเป็นตัวเดียว

# ตรวจสอบ exit code
ls /nonexistent 2>/dev/null
echo "exit code: $?"   # 2 (ไม่พบไฟล์)
ls / >/dev/null
echo "exit code: $?"   # 0 (สำเร็จ)

6.2.5 Read-only Variable (ตัวแปรอ่านอย่างเดียว)

ใช้ readonly หรือ declare -r เพื่อทำให้ตัวแปรเปลี่ยนค่าไม่ได้อีก เหมาะสำหรับค่าคงที่ (Constant) เช่น path สำคัญ, version number, configuration

#!/usr/bin/env bash

readonly VERSION="1.0.0"
readonly LOG_DIR="/var/log/myapp"
declare -r MAX_RETRY=3              # เทียบเท่า readonly

echo "Version: $VERSION"

# พยายามแก้ไข จะ error
VERSION="2.0.0"
# bash: VERSION: readonly variable

# ดูตัวแปร readonly ทั้งหมด list readonly variables
readonly -p
declare -r

6.2.6 Array (อาเรย์)

Bash รองรับ Array สองแบบ: Indexed Array (ดัชนีเป็นตัวเลข เริ่มจาก 0) และ Associative Array (ดัชนีเป็น string คล้าย Hash Map ของ Python หรือ Map ของ JavaScript)

#!/usr/bin/env bash

# ----- Indexed Array -----
fruits=("apple" "banana" "cherry" "durian")

# เข้าถึง element
echo "${fruits[0]}"        # apple (index เริ่มจาก 0)
echo "${fruits[2]}"        # cherry

# เข้าถึงทั้งหมด
echo "${fruits[@]}"        # apple banana cherry durian
echo "${fruits[*]}"        # apple banana cherry durian
echo "${#fruits[@]}"       # 4 (จำนวน element)

# index ทั้งหมด indices
echo "${!fruits[@]}"       # 0 1 2 3

# Slice (เริ่ม:จำนวน)
echo "${fruits[@]:1:2}"    # banana cherry

# เพิ่ม element
fruits+=("elderberry")     # ต่อท้าย
fruits[10]="fig"           # เพิ่มที่ index 10 (ระหว่างจะ sparse)

# ลบ element
unset 'fruits[1]'          # ลบ index 1 (ส่วนที่เหลือไม่ shift)

# วน loop ผ่าน array
for fruit in "${fruits[@]}"; do
    echo "ผลไม้: $fruit"
done

# ----- Associative Array (Bash 4+) -----
declare -A capital
capital["Thailand"]="Bangkok"
capital["Japan"]="Tokyo"
capital["France"]="Paris"

# หรือประกาศพร้อมค่าเริ่มต้น
declare -A scores=(
    ["math"]=85
    ["english"]=72
    ["thai"]=90
)

echo "${capital[Thailand]}"          # Bangkok
echo "${!scores[@]}"                 # math english thai (keys)
echo "${scores[@]}"                  # 85 72 90 (values)

# วน loop ผ่าน associative array
for subject in "${!scores[@]}"; do
    echo "$subject = ${scores[$subject]}"
done

# ตรวจสอบว่ามี key หรือไม่
if [[ -v capital["Thailand"] ]]; then
    echo "มี key Thailand"
fi

6.2.7 การรับค่าจากผู้ใช้ (User Input with read)

คำสั่ง read ใช้รับ input จากผู้ใช้ผ่าน stdin บรรจุลง variable

#!/usr/bin/env bash

# ----- รับค่าง่าย ๆ -----
echo -n "ชื่อของคุณ: "
read name
echo "สวัสดี $name"

# ----- รับค่าพร้อม prompt (-p) -----
read -p "อายุของคุณ: " age
echo "อายุ $age ปี"

# ----- ซ่อนการพิมพ์ (-s) เหมาะกับ password -----
read -sp "รหัสผ่าน: " password
echo                          # ขึ้นบรรทัดใหม่ (เพราะ -s ไม่พิมพ์ newline)
echo "ยาว ${#password} ตัวอักษร"

# ----- กำหนด timeout (-t วินาที) -----
if read -t 5 -p "ตอบใน 5 วินาที: " answer; then
    echo "คำตอบ: $answer"
else
    echo
    echo "หมดเวลา timeout"
fi

# ----- จำกัดจำนวนตัวอักษร (-n) -----
read -n 1 -p "กด y เพื่อยืนยัน: " confirm
echo
[[ "$confirm" == "y" ]] && echo "ยืนยัน" || echo "ยกเลิก"

# ----- รับหลายตัวแปรพร้อมกัน (แยกด้วย IFS) -----
read -p "ชื่อ-นามสกุล: " first last
echo "ชื่อ: $first | นามสกุล: $last"

# ----- รับเป็น array (-a) -----
read -p "ใส่หลายค่าเว้นวรรค: " -a items
echo "ได้ ${#items[@]} ค่า: ${items[@]}"

# ----- รับจาก file ทีละบรรทัด -----
while IFS= read -r line; do
    echo "บรรทัด: $line"
done < /etc/hostname

ตารางสรุป option ของ read:

Option ความหมาย
-p TEXT แสดง prompt
-s silent ไม่แสดง input (password)
-t SEC timeout ในหน่วยวินาที
-n N อ่านแค่ N ตัวอักษร
-r raw mode ไม่ตีความ backslash
-a NAME อ่านเข้า array
-d DELIM ใช้ DELIM เป็นตัวหยุด แทน newline

6.3 การดำเนินการและการคำนวณ (Operations & Computation)

6.3.1 Arithmetic Expansion (การคำนวณตัวเลข)

Bash รองรับการคำนวณ integer ในตัว ผ่าน 3 รูปแบบ: $((...)) (แนะนำ), let และ expr (เก่า)

#!/usr/bin/env bash

# ----- รูปแบบที่แนะนำ: $((...))  -----
a=10
b=3

echo $((a + b))     # 13 บวก
echo $((a - b))     # 7  ลบ
echo $((a * b))     # 30 คูณ
echo $((a / b))     # 3  หาร (integer division ตัดทศนิยม)
echo $((a % b))     # 1  modulo
echo $((a ** b))    # 1000 ยกกำลัง

# ใช้ตัวแปรโดยตรงไม่ต้องใส่ $
echo $((a + 5))     # 15
echo $(((a + b) * 2))  # 26

# ----- bitwise operators -----
echo $((5 & 3))     # 1   AND
echo $((5 | 3))     # 7   OR
echo $((5 ^ 3))     # 6   XOR
echo $((~5))        # -6  NOT
echo $((5 << 1))    # 10  shift left
echo $((20 >> 2))   # 5   shift right

# ----- comparison ใน arithmetic context (คืน 1=true, 0=false) -----
echo $((a > b))     # 1
echo $((a == b))    # 0

# ----- compound assignment -----
((counter++))       # increment เพิ่มทีละ 1
((counter--))       # decrement
((counter += 5))    # counter = counter + 5
((counter *= 2))    # counter = counter * 2

# ----- let -----
let result=10*5+2
echo "$result"      # 52

# ----- expr (รูปแบบเก่า ต้องเว้นวรรค) -----
result=$(expr 10 + 5)
echo "$result"      # 15
result=$(expr 10 \* 5)   # ต้อง escape * เพราะ shell ตีความเป็น glob
echo "$result"      # 50

# ----- ทศนิยม: ใช้ bc หรือ awk -----
echo "scale=4; 10/3" | bc            # 3.3333
awk 'BEGIN { printf "%.4f\n", 10/3 }' # 3.3333

# function สำหรับคำนวณทศนิยม
calc() { awk "BEGIN { print $* }"; }
calc "1.5 * 2.7"      # 4.05
calc "sqrt(16)"       # 4

สมการตัวอย่าง: ค่าเฉลี่ยเลขคณิต (Arithmetic Mean)

x = 1 n i=1 n xi

โดย x คือค่าเฉลี่ย, n คือจำนวนข้อมูล, และ xi คือค่าข้อมูลตัวที่ i

# implement สูตรค่าเฉลี่ยใน shell
calc_mean() {
    local sum=0
    local count=$#
    for n in "$@"; do
        sum=$((sum + n))
    done
    awk "BEGIN { printf \"%.2f\n\", $sum/$count }"
}
calc_mean 85 72 90 78 95   # 84.00

6.3.2 String Manipulation (การจัดการสตริง)

Bash มี Parameter Expansion สำหรับจัดการสตริงได้หลากหลายโดยไม่ต้องเรียกโปรแกรมนอก เช่น sed หรือ awk ทำให้สคริปต์เร็วขึ้น

รูปแบบ ความหมาย ตัวอย่าง
${#var} ความยาว string ${#"hello"} = 5
${var:offset} ตัดตั้งแต่ offset ${var:7}
${var:offset:length} substring ${var:7:5}
${var/pattern/repl} แทนที่ครั้งแรก ${var/foo/bar}
${var//pattern/repl} แทนที่ทั้งหมด ${var//foo/bar}
${var/#pattern/repl} แทนที่ขึ้นต้น
${var/%pattern/repl} แทนที่ลงท้าย
${var#pattern} ตัด prefix สั้นสุด
${var##pattern} ตัด prefix ยาวสุด
${var%pattern} ตัด suffix สั้นสุด
${var%%pattern} ตัด suffix ยาวสุด
${var^} upper ตัวแรก ${var^} = "Hello"
${var^^} upper ทั้งหมด ${var^^} = "HELLO"
${var,} lower ตัวแรก
${var,,} lower ทั้งหมด
#!/usr/bin/env bash

s="Hello World, Linux is Awesome"

# ความยาว length
echo "${#s}"              # 29

# substring
echo "${s:0:5}"           # Hello (เริ่ม 0 ยาว 5)
echo "${s:6}"             # World, Linux is Awesome (จาก index 6 ถึงท้าย)
echo "${s:6:5}"           # World
echo "${s: -7}"           # Awesome (negative ต้องมีเว้นวรรค)
echo "${s: -7:3}"         # Awe

# replace
echo "${s/World/Earth}"   # Hello Earth, Linux is Awesome (ครั้งแรก)
echo "${s//o/0}"          # Hell0 W0rld, Linux is Awes0me (ทั้งหมด)
echo "${s/#Hello/Hi}"     # Hi World, Linux is Awesome (ขึ้นต้นเท่านั้น)
echo "${s/%some/SOME}"    # Hello World, Linux is AweSOME (ลงท้ายเท่านั้น)

# ตัดส่วนนำหน้า (prefix removal)
file="image.tar.gz"
echo "${file#*.}"         # tar.gz   (สั้นสุด)
echo "${file##*.}"        # gz       (ยาวสุด - extension)

# ตัดส่วนท้าย (suffix removal)
echo "${file%.*}"         # image.tar (สั้นสุด)
echo "${file%%.*}"        # image     (ยาวสุด - basename ไม่มี ext)

# upper/lower case
name="moo"
echo "${name^}"           # Moo
echo "${name^^}"          # MOO
echo "${name~}"           # toggle case
greeting="HELLO"
echo "${greeting,}"       # hELLO
echo "${greeting,,}"      # hello

# ตัวอย่างจริง: แยก path
path="/home/moo/projects/script.sh"
echo "directory: ${path%/*}"    # /home/moo/projects
echo "filename:  ${path##*/}"   # script.sh
echo "extension: ${path##*.}"   # sh
echo "basename:  ${path##*/}"
basename="${path##*/}"
echo "name:      ${basename%.*}"  # script

6.3.3 Quoting (การใส่เครื่องหมายคำพูด)

การ Quote เป็นเรื่องสำคัญที่สุดเรื่องหนึ่งใน Shell เพราะส่งผลต่อ Word Splitting, Globbing, Expansion ใช้ผิดอาจทำให้สคริปต์มีบั๊กที่หายาก

รูปแบบ ขยาย $var ขยาย \ ขยาย \...`` Word splitting Glob
ไม่มี quote
"..." (double) ✅ บางตัว
'...' (single)
$'...' (ANSI-C) ✅ พิเศษ
#!/usr/bin/env bash

name="Linus Torvalds"

# ไม่มี quote — อันตราย
echo Hello $name        # Hello Linus Torvalds (แต่ถ้า name="*" จะ glob!)
mkdir test\ dir         # ต้อง escape ช่องว่าง

# Double quote — ใช้บ่อยที่สุด แนะนำ
echo "Hello $name"      # Hello Linus Torvalds (ตัวแปรขยาย)
echo "Path: $HOME"      # Path: /home/moo
echo "Today: $(date)"   # Today: ... (command substitution ทำงาน)
echo "Escape: \$name"   # Escape: $name (\$ ไม่ขยาย)
echo "Newline: line1\nline2"  # ไม่ขึ้นบรรทัด — \n ไม่ทำงานใน double-quote!

# Single quote — literal สมบูรณ์
echo 'Hello $name'      # Hello $name (ตัวแปรไม่ขยาย)
echo 'Cost: $5'         # Cost: $5
# echo 'It\'s'          # ❌ ผิด: ไม่สามารถ escape ใน single-quote
echo "It's"             # It's (ใช้ double-quote แทน)
echo 'It'\''s'          # It's (ปิด-escape-เปิด ใหม่)

# ANSI-C quote $'...' — รองรับ escape
echo $'Line1\nLine2'    # Line1
                        # Line2
echo $'Tab\there'       # Tab     here
echo $'\u00e9'          # é (Unicode)
echo $'\x41'            # A (hex)

# ตัวอย่างที่สำคัญ — ใส่ quote เสมอเมื่อใช้กับ filename
file="my document.txt"
# ผิด: ls $file → ls my document.txt (มอง 2 args)
ls "$file"              # ถูกต้อง

# array ก็เช่นกัน
items=("a b" "c" "d e")
for x in ${items[@]}; do echo "ผิด: [$x]"; done   # 4 รอบ!
for x in "${items[@]}"; do echo "ถูก: [$x]"; done # 3 รอบ

กฎทอง: ใส่ "..." รอบตัวแปรเสมอ ยกเว้นกรณีที่ต้องการ word splitting หรือ globbing โดยตั้งใจจริง ๆ


6.4 Conditional Statements (คำสั่งเงื่อนไข)

คำสั่งเงื่อนไขช่วยให้สคริปต์ตัดสินใจการทำงานตามค่าที่ตรวจสอบ Bash มีโครงสร้างเงื่อนไขแบบ if/elif/else, case, และ short-circuit operators

flowchart TD
    Start([เริ่ม start]) --> C{เงื่อนไข
condition} C -->|true
exit code 0| T[คำสั่งใน then
then block] C -->|false
non-zero| E[คำสั่งใน else
else block] T --> End([จบ end]) E --> End

6.4.1 if / elif / else / fi

โครงสร้างพื้นฐานของ if เช็คเงื่อนไขจาก exit code ของคำสั่ง: 0 = true, อื่น ๆ = false ซึ่งต่างจากภาษาทั่วไปที่ใช้ boolean

#!/usr/bin/env bash

# โครงสร้างพื้นฐาน basic structure
if condition; then
    # ทำเมื่อ true
elif another_condition; then
    # ทำเมื่อ another true
else
    # ทำเมื่อทั้งหมด false
fi

# ----- ตัวอย่างที่ใช้งานจริง -----
score=85

if [[ $score -ge 80 ]]; then
    grade="A"
elif [[ $score -ge 70 ]]; then
    grade="B"
elif [[ $score -ge 60 ]]; then
    grade="C"
elif [[ $score -ge 50 ]]; then
    grade="D"
else
    grade="F"
fi
echo "คะแนน $score ได้เกรด $grade"

# ----- ใช้ exit code โดยตรง -----
if grep -q "ERROR" /var/log/syslog; then
    echo "พบ error ใน log"
fi

# กลับเงื่อนไข negate ด้วย !
if ! ping -c 1 -W 1 8.8.8.8 &>/dev/null; then
    echo "ไม่มี internet"
fi

# ----- One-line if ใช้ ; แยก -----
if [[ -d /tmp ]]; then echo "มี /tmp"; fi

6.4.2 Test File (การทดสอบไฟล์)

Test คำอธิบาย
-e FILE มีอยู่ (existent)
-f FILE เป็น regular file
-d FILE เป็น directory
-L FILE หรือ -h FILE เป็น symlink
-r FILE อ่านได้ (readable)
-w FILE เขียนได้ (writable)
-x FILE execute ได้
-s FILE มีขนาดมากกว่า 0 byte
-b FILE block device
-c FILE character device
-p FILE named pipe (FIFO)
-S FILE socket
-O FILE เจ้าของเป็นผู้ใช้ปัจจุบัน
-G FILE กลุ่มเดียวกับผู้ใช้ปัจจุบัน
FILE1 -nt FILE2 newer than (mtime)
FILE1 -ot FILE2 older than
FILE1 -ef FILE2 inode เดียวกัน (hard link)
#!/usr/bin/env bash

target="/etc/hosts"

# ตรวจสอบประเภทและสิทธิ์ check type & permission
if [[ -e "$target" ]]; then
    echo "✓ มีไฟล์อยู่"
fi

if [[ -f "$target" ]]; then
    echo "✓ เป็น regular file"
elif [[ -d "$target" ]]; then
    echo "✓ เป็น directory"
elif [[ -L "$target" ]]; then
    echo "✓ เป็น symlink"
fi

# ตรวจสอบสิทธิ์
[[ -r "$target" ]] && echo "อ่านได้"
[[ -w "$target" ]] && echo "เขียนได้"
[[ -x "$target" ]] && echo "execute ได้"

# ตรวจสอบขนาด
if [[ -s "$target" ]]; then
    size=$(stat -c%s "$target")
    echo "ไฟล์มีขนาด $size byte"
else
    echo "ไฟล์ว่าง หรือไม่มี"
fi

# เปรียบเทียบเวลา compare timestamps
src="/tmp/source.txt"
dst="/tmp/backup.txt"
touch "$src"; sleep 1; touch "$dst"
if [[ "$src" -nt "$dst" ]]; then
    echo "$src ใหม่กว่า $dst — ต้องสำรองใหม่"
elif [[ "$dst" -nt "$src" ]]; then
    echo "$dst ใหม่กว่า — ทันสมัยแล้ว"
fi

6.4.3 Test String (การทดสอบสตริง)

Test ความหมาย
-z STR ว่างเปล่า (zero length)
-n STR ไม่ว่าง (non-zero length)
STR1 = STR2 เท่ากัน (POSIX)
STR1 == STR2 เท่ากัน (Bash)
STR1 != STR2 ไม่เท่ากัน
STR1 < STR2 น้อยกว่า (lexicographic, ใน [[ ]])
STR1 > STR2 มากกว่า (lexicographic, ใน [[ ]])
STR =~ regex ตรงกับ regex (ใน [[ ]])
#!/usr/bin/env bash

name=""
[[ -z "$name" ]] && echo "ชื่อว่าง"     # true
[[ -n "$name" ]] || echo "ชื่อว่าง"     # true (! -n เทียบเท่า -z)

name="moo"
if [[ "$name" == "moo" ]]; then
    echo "ใช่ moo"
fi

# Pattern matching ใน [[ ]] (ไม่ต้องใช้ regex)
filename="report.pdf"
if [[ "$filename" == *.pdf ]]; then
    echo "เป็น PDF"
fi
if [[ "$filename" == report.* ]]; then
    echo "ขึ้นต้นด้วย report"
fi

# Regular expression =~
email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "อีเมลถูกต้อง"
    echo "ชื่อ: ${BASH_REMATCH[0]}"  # match เต็ม
fi

# เปรียบเทียบ lexicographic
if [[ "apple" < "banana" ]]; then
    echo "apple มาก่อน banana"
fi

# Capture group ใน regex
date_str="2026-04-25"
if [[ "$date_str" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    year="${BASH_REMATCH[1]}"
    month="${BASH_REMATCH[2]}"
    day="${BASH_REMATCH[3]}"
    echo "ปี $year เดือน $month วัน $day"
fi

6.4.4 Test Number (การทดสอบตัวเลข)

Operator ความหมาย สัญลักษณ์เทียบ
-eq equal ==
-ne not equal !=
-lt less than <
-le less than or equal <=
-gt greater than >
-ge greater than or equal >=
#!/usr/bin/env bash

a=10
b=20

# ใช้ใน [ ] หรือ [[ ]] — ใช้ -eq, -ne, -lt, ...
if [[ $a -lt $b ]]; then echo "a น้อยกว่า b"; fi
if [[ $a -eq 10 ]]; then echo "a เท่ากับ 10"; fi
if [[ $a -ne $b ]]; then echo "ไม่เท่ากัน"; fi

# ใช้ใน (( )) — ใช้ ==, !=, <, > ได้เลย
if (( a < b )); then echo "a < b"; fi
if (( a == 10 )); then echo "a == 10"; fi
if (( a + 5 == 15 )); then echo "a+5 = 15"; fi

# ⚠️ ระวัง: ใน [[ ]] ห้ามใช้ < > กับตัวเลข เพราะจะเปรียบเทียบเป็น string!
[[ "10" < "9" ]] && echo "ผิด: 10 < 9 เพราะ '1' < '9' lexicographic"
(( 10 < 9 )) || echo "ถูกต้อง: 10 ไม่น้อยกว่า 9"

6.4.5 [[ ... ]] vs [ ... ] vs test

test, [ ] เป็นคำสั่งภายนอก/builtin POSIX ดั้งเดิม ส่วน [[ ]] เป็น keyword ของ Bash ที่มี Feature เพิ่ม เช่น regex, pattern matching, ไม่ต้องใส่ quote ตัวแปร

คุณสมบัติ [ ] / test [[ ]]
POSIX ❌ Bash/zsh เท่านั้น
ต้อง quote ตัวแปร ✅ จำเป็น ❌ ไม่จำเป็น (แต่แนะนำ)
Word splitting ✅ มี ❌ ไม่มี
Pattern match ==
Regex =~
Logic && || ❌ ใช้ -a -o
< > เปรียบเทียบ string ❌ ต้อง escape \<
#!/usr/bin/env bash

var=""

# [ ] — ต้อง quote เพื่อกัน error
if [ -z "$var" ]; then echo "ว่าง [ ]"; fi
# ถ้าไม่ใส่ quote: [ -z ] → error เพราะ var ขยายเป็นว่าง

# [[ ]] — ไม่ต้อง quote ก็ทำงาน (แต่ยังควร quote)
if [[ -z $var ]]; then echo "ว่าง [[ ]]"; fi

# Logical ใน [ ] — ใช้ -a, -o (deprecated) หรือเชื่อมด้วย && ||
if [ "$a" -gt 0 ] && [ "$a" -lt 100 ]; then echo "อยู่ใน 0-100"; fi

# Logical ใน [[ ]] — ใช้ && || ภายในได้เลย
if [[ $a -gt 0 && $a -lt 100 ]]; then echo "อยู่ใน 0-100"; fi

# คำแนะนำ: ใน Bash ใช้ [[ ]] เสมอ ปลอดภัยกว่า
# ใน sh script (POSIX) ใช้ [ ] เพื่อ portability

6.4.6 Logical Operator (ตัวดำเนินการตรรกะ)

#!/usr/bin/env bash

# ----- Short-circuit evaluation -----
# &&  : AND (รันคำสั่งถัดไปเมื่อตัวแรกสำเร็จ exit 0)
# ||  : OR  (รันคำสั่งถัดไปเมื่อตัวแรกล้มเหลว exit non-0)

# ตัวอย่างการใช้แทน if สั้น ๆ
[[ -d /tmp ]] && echo "มี /tmp"
[[ -d /nonexistent ]] || echo "ไม่มี /nonexistent"

# pattern: command1 && command2 || command3
# ⚠️ ระวัง: ไม่เทียบเท่า if/else เสมอ
mkdir backup && echo "สร้างสำเร็จ" || echo "สร้างไม่สำเร็จ"

# ใช้ใน [[ ]]
age=25
if [[ $age -ge 18 && $age -le 60 ]]; then
    echo "อายุทำงาน"
fi

if [[ $name == "admin" || $name == "root" ]]; then
    echo "บัญชีพิเศษ"
fi

# Negation
if ! command -v docker &>/dev/null; then
    echo "ไม่ได้ติดตั้ง docker"
fi

# ----- ตัวอย่างใช้งานจริง -----
# ตรวจสอบ + ติดตั้งถ้าไม่มี check & install if missing
command -v jq &>/dev/null || sudo apt install -y jq

# สร้างไดเรกทอรีถ้ายังไม่มี
[[ -d ~/backup ]] || mkdir -p ~/backup

# Run only as root
[[ $EUID -eq 0 ]] || { echo "กรุณารันด้วย root"; exit 1; }

6.4.7 case / esac

case เหมาะกับการเปรียบเทียบค่าเดียวกับหลาย pattern เป็นตัวเลือกที่อ่านง่ายกว่า if/elif หลายชั้น และรองรับ glob pattern

#!/usr/bin/env bash

# โครงสร้าง case
case "$VARIABLE" in
    pattern1)
        # คำสั่ง
        ;;
    pattern2 | pattern3)    # หลาย pattern คั่นด้วย |
        # คำสั่ง
        ;;
    *)                       # default (ใช้ * เพราะ glob)
        # คำสั่งเริ่มต้น
        ;;
esac

# ----- ตัวอย่าง: เมนูเลือกการกระทำ -----
read -p "เลือก [s]tart, [r]estart, [q]uit: " action

case "$action" in
    s | start)
        echo "เริ่มต้นบริการ..."
        systemctl start nginx
        ;;
    r | restart)
        echo "รีสตาร์ทบริการ..."
        systemctl restart nginx
        ;;
    q | quit | exit)
        echo "ลาก่อน"
        exit 0
        ;;
    *)
        echo "คำสั่งไม่ถูกต้อง"
        exit 1
        ;;
esac

# ----- ตัวอย่าง: จำแนกไฟล์ตาม extension -----
process_file() {
    local file="$1"
    case "$file" in
        *.jpg | *.jpeg | *.png | *.gif | *.webp)
            echo "$file: รูปภาพ image"
            ;;
        *.mp4 | *.mkv | *.avi | *.mov)
            echo "$file: วิดีโอ video"
            ;;
        *.tar.gz | *.tgz | *.tar.bz2 | *.tar.xz | *.zip)
            echo "$file: archive"
            ;;
        *.sh | *.bash | *.zsh)
            echo "$file: shell script"
            ;;
        *)
            echo "$file: ประเภทอื่น unknown"
            ;;
    esac
}

process_file "photo.jpg"      # รูปภาพ
process_file "movie.mp4"      # วิดีโอ
process_file "archive.tar.gz" # archive

# ----- ตัวอย่าง: ตรวจสอบ OS -----
case "$(uname -s)" in
    Linux*)   os="Linux";;
    Darwin*)  os="macOS";;
    CYGWIN* | MINGW* | MSYS*) os="Windows";;
    FreeBSD*) os="FreeBSD";;
    *)        os="Unknown";;
esac
echo "ระบบปฏิบัติการ: $os"

# ----- ใน Bash 4+ สามารถ fall-through ได้ด้วย ;& และ ;;& -----
case "$x" in
    pattern1)
        echo "p1"
        ;&             # fall-through ไป pattern ถัดไป
    pattern2)
        echo "p2"      # ทำต่อโดยไม่ตรวจ pattern2
        ;;
    pattern3)
        echo "p3"
        ;;&            # ตรวจ pattern ถัดไปอีก
esac

6.5 Loops (วนซ้ำ)

วนซ้ำเป็นโครงสร้างพื้นฐานในการทำงานซ้ำ ๆ Bash มีลูป 3 แบบหลัก: for, while, until พร้อมคำสั่งควบคุม break และ continue

flowchart TD
    subgraph FOR["for loop"]
        F1[เริ่ม init] --> F2{มี item
ถัดไปไหม?} F2 -->|ใช่ yes| F3[ทำคำสั่ง
do block] F3 --> F2 F2 -->|ไม่ no| F4[จบ end] end subgraph WHILE["while loop"] W1[เริ่ม init] --> W2{เงื่อนไข
true?} W2 -->|ใช่ yes| W3[ทำคำสั่ง] W3 --> W2 W2 -->|ไม่ no| W4[จบ] end subgraph UNTIL["until loop"] U1[เริ่ม init] --> U2{เงื่อนไข
true?} U2 -->|ไม่ no| U3[ทำคำสั่ง] U3 --> U2 U2 -->|ใช่ yes| U4[จบ] end FOR ~~~ WHILE ~~~ UNTIL

6.5.1 for loop

#!/usr/bin/env bash

# ----- รูปแบบที่ 1: list-style -----
for fruit in apple banana cherry; do
    echo "ผลไม้: $fruit"
done

# วนผ่าน array
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

# วนผ่าน argument
for arg in "$@"; do
    echo "argument: $arg"
done

# วนผ่านไฟล์ใน directory (glob)
for f in /etc/*.conf; do
    [[ -f "$f" ]] || continue       # ข้ามถ้าไม่มี match
    echo "config: $f"
done

# วนผ่านบรรทัดในไฟล์
while IFS= read -r line; do
    echo "บรรทัด: $line"
done < /etc/hostname

# range expansion (Bash 3+)
for i in {1..5}; do
    echo "ครั้งที่ $i"
done

# range พร้อม step (Bash 4+)
for i in {0..20..2}; do
    echo "$i"   # 0 2 4 6 ... 20
done

# range ตัวอักษร
for c in {a..e}; do
    echo "$c"   # a b c d e
done

# ----- รูปแบบที่ 2: C-style -----
for ((i = 0; i < 10; i++)); do
    echo "i = $i"
done

# C-style พร้อม step
for ((i = 100; i > 0; i -= 10)); do
    echo "$i"   # 100 90 80 ... 10
done

# Multiple variable
for ((i = 0, j = 10; i < 5; i++, j--)); do
    echo "i=$i  j=$j"
done

# ----- ตัวอย่างใช้งานจริง: process หลายไฟล์ -----
for img in *.jpg; do
    [[ -f "$img" ]] || continue
    out="${img%.jpg}.webp"
    echo "Converting $img -> $out"
    # convert "$img" "$out"     # ใช้ ImageMagick
done

# วนผ่าน user ในระบบ
for user in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do
    echo "Regular user: $user"
done

6.5.2 while loop

#!/usr/bin/env bash

# ----- พื้นฐาน basic -----
counter=1
while [[ $counter -le 5 ]]; do
    echo "ครั้งที่ $counter"
    ((counter++))
done

# ----- อ่านไฟล์ทีละบรรทัด — pattern ที่ใช้บ่อย -----
while IFS= read -r line; do
    echo "Got: $line"
done < /etc/hosts

# ----- อ่าน command output -----
ls /tmp | while IFS= read -r f; do
    echo "พบไฟล์: $f"
done
# ⚠️ pipe สร้าง subshell — ตัวแปรแก้ไม่กลับ
# ใช้ Process Substitution แทนถ้าต้องแก้ตัวแปร

count=0
while IFS= read -r f; do
    ((count++))
done < <(ls /tmp)
echo "นับได้ $count ไฟล์"

# ----- ลูปไม่รู้จบ — ระวังใช้ -----
while true; do
    echo "ทำงานทุก 5 วินาที..."
    sleep 5
    [[ -f /tmp/stop ]] && break    # หยุดเมื่อมีไฟล์ flag
done

# ----- รอจน service พร้อม wait until ready -----
while ! curl -s -o /dev/null http://localhost:8080/health; do
    echo "รอ service..."
    sleep 2
done
echo "Service พร้อมแล้ว!"

# ----- หลายเงื่อนไข -----
attempt=0
max=5
while [[ $attempt -lt $max ]] && ! ping -c1 -W1 8.8.8.8 &>/dev/null; do
    ((attempt++))
    echo "ลองครั้งที่ $attempt..."
    sleep 2
done

6.5.3 until loop

until ตรงข้ามกับ while — วนต่อเมื่อเงื่อนไข false หยุดเมื่อ true

#!/usr/bin/env bash

# until = while NOT
counter=1
until [[ $counter -gt 5 ]]; do
    echo "$counter"
    ((counter++))
done

# ----- รอจนกว่าไฟล์จะถูกสร้าง -----
until [[ -f /tmp/done.flag ]]; do
    echo "รอไฟล์ /tmp/done.flag ..."
    sleep 1
done
echo "พบไฟล์แล้ว!"

# ----- รอจน service พร้อม (เทียบเท่า while ! ...) -----
until curl -s http://localhost:8080/health &>/dev/null; do
    sleep 2
done

6.5.4 break และ continue

#!/usr/bin/env bash

# ----- break: ออกจากลูปทันที -----
for i in {1..10}; do
    if [[ $i -eq 5 ]]; then
        break       # ออกที่ 5
    fi
    echo "$i"
done    # พิมพ์ 1 2 3 4

# ----- continue: ข้าม iteration ปัจจุบัน -----
for i in {1..10}; do
    if (( i % 2 == 0 )); then
        continue    # ข้ามเลขคู่
    fi
    echo "$i"
done    # พิมพ์ 1 3 5 7 9

# ----- break/continue หลายชั้น -----
for i in {1..3}; do
    for j in {1..3}; do
        if [[ $i -eq 2 && $j -eq 2 ]]; then
            break 2     # ออก 2 ลูป (ทั้ง outer ด้วย)
        fi
        echo "i=$i j=$j"
    done
done

# ----- continue ลูปนอก -----
for i in {1..3}; do
    for j in {1..3}; do
        if [[ $j -eq 2 ]]; then
            continue 2  # ข้ามไป iteration ถัดไปของ outer
        fi
        echo "i=$i j=$j"
    done
done

6.5.5 seq และ Brace Expansion

#!/usr/bin/env bash

# ----- seq command -----
seq 5                  # 1 2 3 4 5
seq 1 5                # 1 2 3 4 5
seq 1 2 10             # 1 3 5 7 9 (step 2)
seq -w 1 10            # 01 02 ... 10 (zero-padded)
seq -s, 1 5            # 1,2,3,4,5

# ใช้กับ for
for i in $(seq 1 10); do echo "$i"; done

# ----- brace expansion (เร็วกว่า seq เพราะ built-in) -----
echo {1..5}            # 1 2 3 4 5
echo {01..05}          # 01 02 03 04 05
echo {10..1}           # 10 9 8 ... 1 (descending)
echo {1..10..2}        # 1 3 5 7 9 (step, Bash 4+)
echo {a..e}            # a b c d e
echo {Z..A}            # Z Y X ... A

# nested + cartesian product
echo file{1,2,3}.{txt,log}
# file1.txt file1.log file2.txt file2.log file3.txt file3.log

mkdir -p project/{src,test,docs,build}/{main,sub}
# สร้าง 8 directory พร้อมกัน

6.5.6 Infinite Loop และ Polling Pattern

#!/usr/bin/env bash

# ----- รูปแบบ infinite loop -----
while true; do
    # ทำงาน
    sleep 1
done

while :; do                  # : เทียบเท่า true แต่ built-in
    sleep 1
done

for ((;;)); do
    sleep 1
done

# ----- Polling Pattern: ตรวจสอบเงื่อนไขเป็นระยะ -----
poll_log() {
    local logfile="$1"
    local pattern="$2"
    local interval="${3:-5}"

    while true; do
        if grep -q "$pattern" "$logfile" 2>/dev/null; then
            echo "พบ pattern '$pattern' ใน $logfile!"
            return 0
        fi
        sleep "$interval"
    done
}

# poll_log /var/log/nginx/access.log "ERROR" 10

# ----- จับ signal เพื่อออกอย่างสง่างาม graceful shutdown -----
shutdown=0
trap 'shutdown=1' SIGTERM SIGINT

while [[ $shutdown -eq 0 ]]; do
    echo "$(date) — ทำงานอยู่"
    sleep 5
done
echo "ได้รับสัญญาณ shutdown — ปิดงานเรียบร้อย"

# ----- Watch loop พร้อม timestamp -----
monitor_disk() {
    while true; do
        local usage
        usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
        echo "[$(date +%T)] disk usage: ${usage}%"

        if [[ $usage -gt 80 ]]; then
            echo "⚠️ ใกล้เต็ม alert!"
        fi
        sleep 10
    done
}

สมการ: เวลาที่ใช้ในการ Polling เฉลี่ย (Average Polling Wait Time)

E[Twait] = P 2

โดย P คือ Polling Interval (วินาที) และ E[Twait] คือเวลารอเฉลี่ยจนเหตุการณ์ถูกตรวจพบ — ดังนั้น Polling ที่บ่อยขึ้นย่อมตอบสนองเร็วขึ้นแต่ใช้ทรัพยากรมากขึ้น เป็น Trade-off ที่สำคัญ


6.6 Functions (ฟังก์ชัน)

ฟังก์ชันคือบล็อกของคำสั่งที่ตั้งชื่อไว้และเรียกใช้ซ้ำได้ ช่วยให้สคริปต์เป็นระบบ อ่านง่าย และนำกลับมาใช้ใหม่ (DRY — Don't Repeat Yourself)

6.6.1 การประกาศ Function

#!/usr/bin/env bash

# ----- รูปแบบที่ 1: POSIX-style (แนะนำ - portable) -----
greet() {
    echo "สวัสดี"
}

# ----- รูปแบบที่ 2: Bash keyword (แนะนำ POSIX มากกว่า) -----
function greet2 {
    echo "Hi"
}

# ----- รูปแบบที่ 3: รวมทั้งสอง (ไม่จำเป็น) -----
function greet3() {
    echo "Hello"
}

# เรียกใช้ — ไม่ต้องมีวงเล็บ
greet
greet2

# ฟังก์ชันต้องประกาศก่อนเรียกใช้
# main() ต้องอยู่ท้ายไฟล์ หรือเรียกหลังประกาศ

6.6.2 การส่งและรับพารามิเตอร์

ฟังก์ชันรับ argument ผ่านตัวแปรพิเศษเหมือนสคริปต์: $1, $2, ..., $@, $# ($0 ยังคงเป็นชื่อสคริปต์ ไม่ใช่ชื่อฟังก์ชัน)

#!/usr/bin/env bash

# ฟังก์ชันรับ 2 argument
add() {
    local a="$1"
    local b="$2"
    echo $((a + b))
}

result=$(add 5 3)
echo "5 + 3 = $result"     # 8

# ฟังก์ชันรับจำนวน argument ไม่จำกัด
sum_all() {
    local total=0
    for n in "$@"; do
        total=$((total + n))
    done
    echo "$total"
}

echo "Sum: $(sum_all 1 2 3 4 5)"     # 15
echo "Sum: $(sum_all 10 20 30)"      # 60

# ตรวจสอบจำนวน argument
greet() {
    if [[ $# -lt 1 ]]; then
        echo "Usage: greet NAME [GREETING]" >&2
        return 1
    fi
    local name="$1"
    local greeting="${2:-Hello}"      # default Hello
    echo "$greeting, $name!"
}

greet "Moo"            # Hello, Moo!
greet "Moo" "สวัสดี"   # สวัสดี, Moo!
greet                  # error และ return 1

6.6.3 Return Value (ค่าส่งกลับ)

Bash function "return" 2 แบบ: exit code (0-255) ผ่าน return หรือ string output ผ่าน echo/printf แล้วใช้ command substitution $(...) รับค่า

#!/usr/bin/env bash

# ----- return เป็น exit code (0-255) -----
is_even() {
    local n="$1"
    if (( n % 2 == 0 )); then
        return 0       # 0 = สำเร็จ = true
    else
        return 1       # อื่น = ผิดพลาด = false
    fi
}

if is_even 10; then echo "10 is even"; fi
if is_even 7; then echo "7 is even"; else echo "7 is odd"; fi

# ----- return ค่าจริง ๆ ใช้ echo + command substitution -----
square() {
    local n="$1"
    echo $((n * n))
}

result=$(square 5)
echo "5² = $result"       # 25

# ใช้ใน expression
total=$(($(square 3) + $(square 4)))
echo "3² + 4² = $total"   # 25

# ----- กรณีต้องการคืนค่าหลายตัว ใช้ array หรือ global -----
parse_url() {
    local url="$1"
    # ตั้งตัวแปร global (หรือ caller สามารถ declare ก่อน)
    URL_PROTO="${url%%://*}"
    URL_REST="${url#*://}"
    URL_HOST="${URL_REST%%/*}"
    URL_PATH="/${URL_REST#*/}"
}

parse_url "https://example.com/path/to/page"
echo "proto: $URL_PROTO"  # https
echo "host:  $URL_HOST"   # example.com
echo "path:  $URL_PATH"   # /path/to/page

# ----- Pattern: ใช้ nameref (Bash 4.3+) คืนค่าผ่านชื่อตัวแปร -----
get_user_info() {
    local -n result_ref=$1     # nameref
    result_ref[name]="$USER"
    result_ref[home]="$HOME"
    result_ref[shell]="$SHELL"
}

declare -A info
get_user_info info
echo "name=${info[name]}, home=${info[home]}"

6.6.4 Local Variable ภายใน Function

ตัวแปรภายในฟังก์ชันโดยปริยายเป็น global การใช้ local ป้องกัน Side Effect

#!/usr/bin/env bash

x=10

# ❌ ไม่ใช้ local — แก้ x global โดยไม่ตั้งใจ
bad_func() {
    x=99
    echo "in func: $x"
}

bad_func           # 99
echo "after: $x"   # 99 — เปลี่ยนแล้ว!

# ✅ ใช้ local — ปลอดภัย
x=10
good_func() {
    local x=99
    echo "in func: $x"
}

good_func          # 99
echo "after: $x"   # 10 — ไม่เปลี่ยน

# local รองรับ option เหมือน declare
demo() {
    local -i num=42       # integer
    local -r CONST=100    # readonly
    local -a arr=(1 2 3)  # array
    local -A map          # associative array

    echo "num=$num, CONST=$CONST"
    echo "arr=${arr[@]}"
}
demo

6.6.5 Recursion

Bash รองรับ recursion แต่ระวัง stack depth (default ~1000 ซ้อน) และไม่มี tail call optimization จึงใช้กับงานเชิงวิเคราะห์ขนาดเล็กดีกว่า

#!/usr/bin/env bash

# ----- Factorial: n! -----
factorial() {
    local n="$1"
    if (( n <= 1 )); then
        echo 1
    else
        local prev
        prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

echo "5! = $(factorial 5)"     # 120
echo "10! = $(factorial 10)"   # 3628800

# ----- Fibonacci -----
fib() {
    local n="$1"
    if (( n < 2 )); then
        echo "$n"
    else
        echo $(( $(fib $((n - 1))) + $(fib $((n - 2))) ))
    fi
}

for i in {0..10}; do
    echo "fib($i) = $(fib $i)"
done

# ----- ค้นหาไฟล์แบบ recursive (เลียนแบบ find) -----
walk() {
    local dir="$1"
    local indent="${2:-}"

    for entry in "$dir"/*; do
        [[ -e "$entry" ]] || continue
        echo "${indent}$(basename "$entry")"

        if [[ -d "$entry" ]]; then
            walk "$entry" "  $indent"
        fi
    done
}

walk /etc/nginx

สมการ: Factorial นิยามแบบ Recursive

n! = { 1 ถ้า n1 n×(n-1)! ถ้า n>1

โดย n! เรียกตัวเองด้วย (n-1)! จนถึง base case


6.7 Arrays (อาเรย์เชิงลึก)

ส่วนนี้ขยายความเรื่อง Array ที่ได้กล่าวเบื้องต้นใน 6.2.6 ลงรายละเอียดการใช้งานขั้นสูง การวนซ้ำ และตัวอย่างการประยุกต์

6.7.1 Indexed Array

Indexed Array คือ array ที่ใช้เลขจำนวนเต็มเป็น index เริ่มจาก 0 และอนุญาตให้เป็น sparse (มีช่องว่าง)

#!/usr/bin/env bash

# ----- การประกาศ declaration -----
fruits=("apple" "banana" "cherry")
declare -a numbers=(10 20 30 40 50)         # ระบุชนิดชัดเจน
nums[0]=1; nums[5]=99                       # sparse — มี gap

# กำหนดทีละ element
arr=()                # array ว่าง
arr[0]="zero"
arr[1]="one"
arr[2]="two"

# ----- การเข้าถึง access -----
echo "${fruits[0]}"           # apple (index 0)
echo "${fruits[-1]}"          # cherry (index ลบ Bash 4.3+)
echo "${fruits[@]}"           # apple banana cherry (all elements)
echo "${fruits[*]}"           # apple banana cherry
echo "${#fruits[@]}"          # 3 (length)
echo "${#fruits[0]}"          # 5 (length ของ "apple")

# ----- index ที่มีอยู่ -----
echo "${!fruits[@]}"          # 0 1 2

# ----- slice -----
arr=(a b c d e f g h)
echo "${arr[@]:2}"            # c d e f g h (จาก 2 ถึงท้าย)
echo "${arr[@]:2:3}"          # c d e (จาก 2 ยาว 3)
echo "${arr[@]: -2}"          # g h (2 ตัวสุดท้าย — เว้นวรรคก่อน -)

# ----- เพิ่ม element -----
fruits+=("durian")            # ต่อท้าย
fruits+=("elderberry" "fig")  # หลายตัว
fruits[10]="grape"            # เฉพาะตำแหน่ง

# ----- ลบ element -----
unset 'fruits[1]'             # ลบ index 1 (sparse — index 0,2,3,...)
unset fruits                  # ลบทั้ง array

6.7.2 Associative Array

Associative Array (Bash 4+) ใช้ string เป็น key คล้าย Hash Map / Dictionary

#!/usr/bin/env bash

# ต้องประกาศด้วย declare -A ก่อน
declare -A user_info

# กำหนดค่า
user_info["name"]="Moo"
user_info["age"]=42
user_info["email"]="moo@example.com"
user_info["role"]="lecturer"

# หรือสร้างพร้อมค่าเริ่มต้น
declare -A config=(
    [host]="localhost"
    [port]=8080
    [protocol]="https"
    [timeout]=30
)

# ----- เข้าถึง access -----
echo "${user_info[name]}"        # Moo
echo "${config[port]}"           # 8080

# ทุก key
echo "${!user_info[@]}"          # name age email role (ไม่เรียงลำดับ)

# ทุกค่า
echo "${user_info[@]}"           # Moo 42 moo@example.com lecturer

# จำนวน
echo "${#user_info[@]}"          # 4

# ----- ตรวจสอบว่ามี key หรือไม่ -----
if [[ -v user_info[name] ]]; then
    echo "มี key 'name'"
fi
# หรือ
if [[ ${user_info[unknown]+x} ]]; then
    echo "มี key unknown"
else
    echo "ไม่มี key unknown"
fi

# ----- ลบ key -----
unset 'user_info[email]'

# ----- วนผ่าน iterate -----
for key in "${!user_info[@]}"; do
    echo "$key = ${user_info[$key]}"
done

# เรียงลำดับ key
for key in $(echo "${!user_info[@]}" | tr ' ' '\n' | sort); do
    echo "$key = ${user_info[$key]}"
done

6.7.3 การเข้าถึง Element

#!/usr/bin/env bash

arr=(a b c d e f g)

# === การเข้าถึงค่า value ===
echo "${arr[0]}"           # a
echo "${arr[3]}"           # d
echo "${arr[-1]}"          # g (Bash 4.3+)

# === ทุกค่า all values ===
echo "${arr[@]}"           # a b c d e f g
printf '%s\n' "${arr[@]}"  # ทีละบรรทัด

# === ทุก index all keys ===
echo "${!arr[@]}"          # 0 1 2 3 4 5 6

# === ความยาว length ===
echo "${#arr[@]}"          # 7 (จำนวน element)
echo "${#arr[2]}"          # 1 (ความยาวของ arr[2]="c")

# === ความแตกต่าง @ vs * ===
arr=("hello world" "foo bar" "baz")

for x in "${arr[@]}"; do echo "[$x]"; done
# [hello world]
# [foo bar]
# [baz]

for x in "${arr[*]}"; do echo "[$x]"; done
# [hello world foo bar baz]   ← รวมเป็น string เดียว

IFS='|'
for x in "${arr[*]}"; do echo "[$x]"; done
# [hello world|foo bar|baz]   ← ใช้ IFS เชื่อม

# === Slice ===
echo "${arr[@]:1}"          # foo bar baz (จาก index 1)
echo "${arr[@]:0:2}"        # hello world foo bar (2 ตัวแรก)

# === Search/Replace ===
arr=(apple banana cherry)
echo "${arr[@]/an/AN}"      # apple bANana cherry → ผิด: ทำ subst ทุกตัว
# → apple bANana cherry (เปลี่ยนใน element ที่มี match)

# === ลบ element ===
unset 'arr[1]'              # ลบ banana → array sparse
echo "${arr[@]}"            # apple cherry

# Compact (ลบ gap)
arr=("${arr[@]}")           # rebuild
echo "${!arr[@]}"           # 0 1 (ไม่ใช่ 0 2)

6.7.4 การวนซ้ำผ่าน Array

#!/usr/bin/env bash

# ===== Indexed Array =====
fruits=("apple" "banana" "cherry" "durian")

# วิธีที่ 1: วนผ่านค่า by value
for fruit in "${fruits[@]}"; do
    echo "fruit: $fruit"
done

# วิธีที่ 2: วนผ่าน index by index
for i in "${!fruits[@]}"; do
    echo "$i: ${fruits[$i]}"
done

# วิธีที่ 3: C-style ใช้ index ตัวเลข
for ((i = 0; i < ${#fruits[@]}; i++)); do
    echo "$i: ${fruits[$i]}"
done

# ===== Associative Array =====
declare -A scores=(
    [math]=85
    [english]=72
    [thai]=90
    [science]=88
)

# วนผ่าน key + value
for subject in "${!scores[@]}"; do
    echo "$subject: ${scores[$subject]}"
done

# ===== ตัวอย่าง: หาคะแนนสูงสุด -----
max_subject=""
max_score=0
for subject in "${!scores[@]}"; do
    if (( scores[$subject] > max_score )); then
        max_score=${scores[$subject]}
        max_subject=$subject
    fi
done
echo "วิชาคะแนนสูงสุด: $max_subject ($max_score)"

# ===== ตัวอย่าง: นับความถี่ของคำ -----
declare -A word_count
text="the quick brown fox jumps over the lazy dog the end"
for word in $text; do
    ((word_count[$word]++))
done

for w in "${!word_count[@]}"; do
    echo "$w: ${word_count[$w]}"
done

# ===== ตัวอย่าง: parallel arrays -----
names=("Alice" "Bob" "Charlie")
ages=(30 25 35)
roles=("admin" "user" "user")

for i in "${!names[@]}"; do
    printf "%-10s | %2d ปี | %s\n" \
        "${names[$i]}" "${ages[$i]}" "${roles[$i]}"
done

6.8 Input/Output ขั้นสูง (Advanced I/O)

6.8.1 Here Document (<<EOF)

Here Document คือการส่ง multiline string เข้าทาง stdin ของคำสั่ง ใช้บ่อยกับการสร้างไฟล์ config, การส่ง SQL/SSH command, หรือการเขียนไฟล์ template

#!/usr/bin/env bash

# ----- รูปแบบพื้นฐาน -----
cat <<EOF
สวัสดีทุกคน
ฉันคือบรรทัดที่สอง
$(date)                           # คำสั่งจะถูกขยาย
ผู้ใช้ปัจจุบันคือ $USER             # ตัวแปรขยาย
EOF

# ----- ปิดการขยายตัวแปร — ใส่ quote รอบ delimiter -----
cat <<'EOF'
ตัวแปร $USER จะไม่ถูกขยาย
$(date) ก็ไม่ทำงาน
ใช้สำหรับเขียน script template
EOF

# ----- ตัด tab นำหน้าด้วย <<- (สำหรับ indent) -----
if true; then
	cat <<-EOF
	บรรทัดนี้มี tab นำหน้า
	แต่ <<- จะตัดออก ทำให้ output ดู clean
	หมายเหตุ: ตัดเฉพาะ tab ไม่ตัด space
	EOF
fi

# ----- ใช้สร้างไฟล์ -----
cat > /tmp/config.yaml <<EOF
server:
  host: localhost
  port: 8080
  user: $USER
  pid: $$
EOF

# ----- ใช้ส่งคำสั่ง SSH -----
ssh user@remote-host <<'ENDSSH'
echo "ผมอยู่บน $(hostname)"
uptime
df -h /
ENDSSH

# ----- ใช้กับ SQL -----
mysql -u root -p"$DB_PASS" <<SQL
USE myapp;
SELECT COUNT(*) FROM users WHERE active = 1;
UPDATE settings SET value = 'true' WHERE key = 'maintenance';
SQL

# ----- เขียนสคริปต์ Python ใน Bash -----
python3 <<'PYEOF'
import sys
print("Python within Bash!")
print(f"sys.argv: {sys.argv}")
PYEOF

# ----- เก็บลงตัวแปร -----
my_text=$(cat <<EOF
บรรทัดที่ 1
บรรทัดที่ 2
บรรทัดที่ 3
EOF
)
echo "$my_text"

6.8.2 Here String (<<<)

Here String ส่งข้อความบรรทัดเดียวเข้า stdin (เปรียบเหมือน echo "..." | cmd แต่เร็วกว่าและไม่ fork)

#!/usr/bin/env bash

# ส่ง string เข้า stdin
grep "moo" <<< "moo is awesome"            # match
wc -w <<< "นับ คำ ใน ข้อความ"               # 4

# เทียบกับ pipe — here string ไม่สร้าง subshell
echo "hello" | read x
echo "$x"      # ว่าง! เพราะ read ทำใน subshell

read x <<< "hello"
echo "$x"      # hello — read ทำใน shell ปัจจุบัน

# ส่ง output ของคำสั่ง
read -r host <<< "$(hostname)"
echo "Host: $host"

# ----- ใช้กับ regex -----
phone="081-234-5678"
[[ "$phone" =~ ^([0-9]{3})-([0-9]{3})-([0-9]{4})$ ]] \
    && echo "valid phone: ${BASH_REMATCH[0]}"

# ตัดส่วนของ string เข้า array
input="apple,banana,cherry"
IFS=',' read -ra fruits <<< "$input"
for f in "${fruits[@]}"; do
    echo "fruit: $f"
done

6.8.3 getopts สำหรับ Argument Parsing

getopts เป็น builtin สำหรับแยก option flag (-a, -b, -c) แบบ POSIX-compatible

#!/usr/bin/env bash
# usage: ./script.sh -u USER -p PORT [-v] [-h]

usage() {
    cat <<EOF
Usage: $0 -u USER -p PORT [-v] [-h]

Options:
  -u USER     ชื่อผู้ใช้ (จำเป็น)
  -p PORT     พอร์ต (default 22)
  -v          verbose mode
  -h          แสดงคู่มือ

Example:
  $0 -u admin -p 2222 -v
EOF
    exit 1
}

# ค่าเริ่มต้น
user=""
port=22
verbose=0

# ":u:p:vh" — ตัวอักษรที่มี : ตามหลังคือ option ที่ต้องมีค่า
# ขึ้นต้นด้วย : ทำให้ getopts จัดการ error เอง (silent mode)
while getopts ":u:p:vh" opt; do
    case "$opt" in
        u) user="$OPTARG" ;;
        p) port="$OPTARG" ;;
        v) verbose=1 ;;
        h) usage ;;
        :) echo "Error: -$OPTARG ต้องมีค่า" >&2; usage ;;
        ?) echo "Error: option ไม่รู้จัก -$OPTARG" >&2; usage ;;
    esac
done

# shift options ออก เหลือ argument ที่ไม่ใช่ option
shift $((OPTIND - 1))

# ตรวจสอบ required
if [[ -z "$user" ]]; then
    echo "Error: -u USER จำเป็น" >&2
    usage
fi

echo "User    : $user"
echo "Port    : $port"
echo "Verbose : $verbose"
echo "Other args : $@"

# ----- หมายเหตุ: getopts รองรับเฉพาะ short option (-x) -----
# สำหรับ long option (--option) ต้องใช้ getopt (จาก util-linux) หรือเขียน parser เอง

ตัวอย่าง parser สำหรับ long option (manual parsing):

while [[ $# -gt 0 ]]; do
    case "$1" in
        -u | --user)
            user="$2"; shift 2 ;;
        -p | --port)
            port="$2"; shift 2 ;;
        -v | --verbose)
            verbose=1; shift ;;
        --user=*)
            user="${1#*=}"; shift ;;
        -h | --help)
            usage ;;
        --)
            shift; break ;;
        -*)
            echo "Unknown option: $1" >&2; usage ;;
        *)
            args+=("$1"); shift ;;
    esac
done

6.8.4 select สำหรับ Interactive Menu

select builtin สร้าง menu เลขเลือกอัตโนมัติ ใช้ทำเครื่องมือ interactive

#!/usr/bin/env bash

PS3="กรุณาเลือก action: "      # prompt ของ select

select action in "Start" "Stop" "Restart" "Status" "Quit"; do
    case "$action" in
        Start)
            echo "เริ่มต้น..."
            ;;
        Stop)
            echo "หยุดทำงาน..."
            ;;
        Restart)
            echo "รีสตาร์ท..."
            ;;
        Status)
            echo "ตรวจสอบสถานะ..."
            ;;
        Quit)
            echo "ออก..."
            break
            ;;
        *)
            echo "ตัวเลือกไม่ถูกต้อง"
            ;;
    esac
done

# ----- เลือกจากไฟล์ใน directory -----
echo "เลือกไฟล์ที่ต้องการดู:"
select file in *.log; do
    if [[ -n "$file" ]]; then
        less "$file"
        break
    fi
done

# ----- เลือกจากหลาย source -----
options=("/etc/passwd" "/etc/group" "/etc/hosts" "Quit")
PS3="ไฟล์ไหน? "
select choice in "${options[@]}"; do
    case "$choice" in
        Quit) break ;;
        "") echo "เลือกผิด"; continue ;;
        *) cat "$choice"; break ;;
    esac
done

6.8.5 IFS (Internal Field Separator)

IFS เป็นตัวแปรที่ shell ใช้เป็นตัวแยก field ใน word splitting ค่า default คือ space, tab, newline ($' \t\n')

#!/usr/bin/env bash

# ดู IFS ปัจจุบัน
echo "IFS: $(printf '%q' "$IFS")"   # แสดง $' \t\n'

# ----- เปลี่ยน IFS เพื่อ split string -----
csv="apple,banana,cherry,durian"

# default IFS — ไม่ split ตาม comma
arr1=($csv)
echo "ใช้ default IFS: ${#arr1[@]} ตัว"   # 1

# ใช้ comma เป็น IFS
IFS=',' read -ra arr2 <<< "$csv"
echo "ใช้ IFS=',': ${#arr2[@]} ตัว"      # 4
for f in "${arr2[@]}"; do echo "- $f"; done

# ----- pattern: เปลี่ยน IFS ชั่วคราวเพื่อ split path -----
path="/usr/local/bin"
IFS='/' read -ra parts <<< "$path"
for p in "${parts[@]}"; do echo "[$p]"; done
# []
# [usr]
# [local]
# [bin]

# ----- pattern: save & restore IFS -----
oldIFS=$IFS
IFS='|'
# ทำงาน
IFS=$oldIFS

# ----- ใช้ subshell เพื่อ scope IFS -----
(
    IFS=':'
    for p in $PATH; do
        echo "PATH dir: $p"
    done
)
# IFS กลับเป็นค่าเดิมหลังออก subshell

# ----- อ่านไฟล์ CSV -----
while IFS=',' read -r name age city; do
    echo "$name | $age | $city"
done <<EOF
Alice,30,Bangkok
Bob,25,Chiang Mai
Charlie,35,Phuket
EOF

# ----- ระวัง: IFS เปล่าทำให้คงทุก whitespace -----
while IFS= read -r line; do      # IFS ว่าง — เก็บ leading/trailing space
    echo "[$line]"
done < /etc/issue

6.9 การจัดการข้อผิดพลาด (Error Handling)

Shell Script เริ่มต้นมาแบบ "ถูกใจมือใหม่" คือยังคงทำงานต่อแม้ว่าคำสั่งจะล้มเหลว ทำให้บั๊กยากตรวจ ผู้พัฒนาที่ดีควรเปิดโหมดเข้มงวดและจัดการ error อย่างระมัดระวัง

flowchart LR
    S[Script Start] --> O[set -euo pipefail
เปิดโหมดเข้มงวด] O --> T[trap ERR
จับ error] T --> R[Run command
คำสั่งทำงาน] R -->|success exit 0| C[continue ทำต่อ] R -->|error non-zero| H[trap handler
cleanup + exit] C --> X[exit 0
ออกปกติ] H --> X

6.9.1 Exit Code และ exit N

Process ทุกตัวคืน exit code (status) เป็นเลข 0-255 เมื่อจบ ค่ามาตรฐานคือ 0 = สำเร็จ, ไม่ใช่ 0 = ผิดพลาด

Exit Code ความหมายทั่วไป
0 สำเร็จ (success)
1 error ทั่วไป
2 misuse of shell builtin (เช่น syntax)
126 คำสั่งไม่สามารถ execute (permission denied)
127 command not found
128 invalid argument to exit
128+N ถูก kill ด้วย signal N (130 = SIGINT/Ctrl+C)
255 exit status out of range
#!/usr/bin/env bash

# ดู exit code ของคำสั่งล่าสุด
ls /etc
echo "exit: $?"        # 0

ls /nonexistent 2>/dev/null
echo "exit: $?"        # 2

# ตั้ง exit code ของสคริปต์เอง
do_work() {
    if [[ ! -f "$1" ]]; then
        echo "Error: ไม่พบไฟล์ $1" >&2
        return 1                    # error code 1
    fi
    return 0
}

do_work /etc/hosts
echo "result: $?"      # 0

do_work /no/such/file
echo "result: $?"      # 1

# exit ออกจากสคริปต์ทันที
[[ -f /tmp/lock ]] && { echo "มี lock file"; exit 100; }

# ----- รูปแบบที่นิยม: defined exit codes -----
readonly E_OK=0
readonly E_ARG=1
readonly E_NOFILE=2
readonly E_NOPERM=3
readonly E_NETWORK=4

main() {
    [[ $# -lt 1 ]] && exit $E_ARG
    [[ ! -f "$1" ]] && exit $E_NOFILE
    [[ ! -r "$1" ]] && exit $E_NOPERM
    # ... ทำงาน
    exit $E_OK
}

6.9.2 set Options (Strict Mode)

#!/usr/bin/env bash

# ----- set -e (errexit): หยุดทันทีเมื่อคำสั่งล้มเหลว -----
set -e
ls /nonexistent       # error → exit ทันที
echo "ไม่ถึงบรรทัดนี้"

# ----- set -u (nounset): error ถ้าใช้ตัวแปรที่ไม่ได้ตั้ง -----
set -u
echo "$undefined"     # bash: undefined: unbound variable → exit

# ----- set -x (xtrace): พิมพ์คำสั่งก่อนรัน — debug -----
set -x
name="moo"
echo "Hello $name"
# + name=moo
# + echo 'Hello moo'
# Hello moo
set +x                # ปิด trace

# ----- set -o pipefail: pipe ล้มเหลวถ้าคำสั่งใดในนั้นล้ม -----
set -o pipefail
cat /nonexistent | grep x | wc -l
# ปกติ exit code = wc (สำเร็จ → 0)
# pipefail → exit code = cat (1 → ล้มเหลว)

# ----- รวม strict mode (Best Practice) -----
set -euo pipefail
IFS=$'\n\t'           # ลด edge case ของ word splitting

# ----- ตัวอย่างเต็ม -----
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# ตัวเลือกอื่น
set -E      # trap ERR ทำงานใน function/subshell
set -T      # trap DEBUG ทำงานใน function

# ปิด errexit ชั่วคราว — ใน statement เดียว
set +e
some_command_might_fail
result=$?
set -e

# หรือใช้ || เพื่อข้าม errexit
some_command_might_fail || true

# ใน if/while errexit ไม่ active โดยอัตโนมัติ
if some_command; then ...; fi    # ไม่ exit แม้ล้มเหลว

6.9.3 trap สำหรับจับ Signal

trap กำหนด handler เมื่อสคริปต์ได้รับ signal หรือเหตุการณ์พิเศษ ใช้สำหรับ cleanup, logging, atomic operation

#!/usr/bin/env bash

# ----- รายชื่อ signal/event ที่สำคัญ -----
# EXIT  : ก่อนสคริปต์จบ (ทุกกรณี)
# ERR   : เมื่อคำสั่งคืน non-zero (ต้องใช้ set -E)
# INT   : Ctrl+C (SIGINT)
# TERM  : kill (SIGTERM)
# HUP   : SIGHUP (terminal ปิด)
# DEBUG : ก่อนทุกคำสั่ง

# ----- pattern: cleanup ก่อนออก -----
TMPDIR=$(mktemp -d)
echo "ทำงานใน $TMPDIR"

cleanup() {
    echo "กำลัง cleanup..."
    rm -rf "$TMPDIR"
    echo "ลบ $TMPDIR แล้ว"
}
trap cleanup EXIT

# ทำงาน
touch "$TMPDIR/data.txt"
echo "test" > "$TMPDIR/data.txt"
# เมื่อจบ trap จะเรียก cleanup อัตโนมัติ

# ----- จับ Ctrl+C เพื่อสรุปก่อนออก -----
processed=0
on_interrupt() {
    echo
    echo "ถูกหยุดที่ครั้งที่ $processed"
    exit 130
}
trap on_interrupt INT

for i in {1..1000}; do
    processed=$i
    sleep 0.5
done

# ----- จับ ERR เพื่อ log error -----
set -eE                   # -E ทำให้ trap ERR สืบทอดใน function

on_error() {
    local exit_code=$?
    local line_no=${BASH_LINENO[0]}
    local cmd="$BASH_COMMAND"
    echo "ERROR: คำสั่ง '$cmd' ล้มเหลวที่บรรทัด $line_no (exit $exit_code)" >&2
}
trap on_error ERR

ls /nonexistent           # จะ trigger trap

# ----- กำหนด trap หลาย signal พร้อมกัน -----
trap 'echo "ได้รับสัญญาณหยุด"; exit' INT TERM HUP

# ----- ลบ trap -----
trap - EXIT               # ลบ trap EXIT
trap - INT TERM           # ลบหลายตัว

6.9.4 Debugging Techniques

#!/usr/bin/env bash

# ----- Method 1: bash -x ทั้งสคริปต์ -----
# bash -x ./script.sh
# พิมพ์ทุกคำสั่งก่อนรัน

# ----- Method 2: bash -n syntax check (ไม่รัน) -----
# bash -n ./script.sh
# ตรวจ syntax error โดยไม่ execute

# ----- Method 3: set -x เฉพาะส่วน -----
echo "ปกติ"
set -x
problematic_section() {
    name="moo"
    echo "$name"
}
problematic_section
set +x
echo "ต่อไป"

# ----- Method 4: ปรับแต่ง PS4 ให้ debug ดียิ่งขึ้น -----
export PS4='+ ${BASH_SOURCE}:${LINENO} ${FUNCNAME[0]:-main}() : '
set -x
my_func() {
    echo "test"
}
my_func
# + script.sh:5 main() : my_func
# + script.sh:3 my_func() : echo test
# + script.sh:3 my_func() : test

# ----- Method 5: function ช่วย log เป็นระดับ -----
LOG_LEVEL="${LOG_LEVEL:-INFO}"

log() {
    local level="$1"; shift
    local levels=(DEBUG INFO WARN ERROR)
    local current=0
    for i in "${!levels[@]}"; do
        [[ "${levels[$i]}" == "$LOG_LEVEL" ]] && current=$i
    done
    local this=0
    for i in "${!levels[@]}"; do
        [[ "${levels[$i]}" == "$level" ]] && this=$i
    done
    if (( this >= current )); then
        printf '[%s] [%s] %s\n' \
            "$(date '+%F %T')" "$level" "$*" >&2
    fi
}

log DEBUG "ค่าตัวแปร x=$x"     # ไม่แสดงถ้า LOG_LEVEL=INFO
log INFO  "เริ่มต้นทำงาน"
log WARN  "ไฟล์ขนาดใหญ่"
log ERROR "เชื่อมต่อล้มเหลว"

# ----- Method 6: ทดสอบสคริปต์แบบ verbose -----
run_safe() {
    echo "+ $*"           # echo คำสั่งก่อน
    "$@"                  # ค่อยรัน
}
run_safe ls /tmp
run_safe echo "test"

6.9.5 ShellCheck

ShellCheck (shellcheck.net) คือ static analyzer สำหรับ shell script ที่ตรวจหาปัญหาและแนะนำการแก้ไข เป็นเครื่องมือบังคับสำหรับนักเขียนสคริปต์มืออาชีพ

# ติดตั้ง
sudo pacman -S shellcheck      # Arch
sudo apt install shellcheck    # Debian/Ubuntu
sudo dnf install ShellCheck    # Fedora

# ใช้งาน
shellcheck script.sh

# ตัวอย่าง output (ปัญหาที่พบบ่อย)
# In script.sh line 5:
# echo $name
#      ^---^ SC2086: Double quote to prevent globbing and word splitting.

คำเตือนยอดฮิต (รหัส SC):

รหัส ความหมาย แก้ไข
SC2086 ตัวแปรไม่ใส่ quote echo "$var"
SC2046 command substitution ไม่ใส่ quote cmd "$(...)"
SC2155 declare + assign แยกบรรทัด local x; x=$(...)
SC2034 ตัวแปรไม่ได้ใช้งาน ลบออกหรือ export
SC2164 cd อาจล้มเหลว cd dir || exit 1
SC1090 source ตัวแปรเปลี่ยนได้ # shellcheck disable=SC1090
# ใส่ comment เพื่อ disable เฉพาะกรณี
# shellcheck disable=SC2034
unused_var="ตั้งใจทิ้งไว้"

# ใช้ใน CI
shellcheck -S error script.sh   # severity = error เท่านั้น
shellcheck --format=gcc *.sh    # format กับ IDE

6.10 Best Practices (แนวปฏิบัติที่ดี)

6.10.1 ตั้งชื่อตัวแปร/ฟังก์ชันให้สื่อความหมาย

# ❌ ไม่ดี
a=10
b=$a
c() { echo "$1"; }

# ✅ ดีกว่า
max_retry=10
current_attempt=$max_retry
display_message() { echo "$1"; }

# ----- กฎทั่วไป -----
# - constant/global: UPPER_SNAKE_CASE
# - local variable:  lower_snake_case
# - function:        snake_case
# - private function: _underscore_prefix
# - environment:     UPPER_SNAKE_CASE export

readonly MAX_RETRY=5
readonly CONFIG_PATH="/etc/myapp/config.yaml"
readonly LOG_FILE="/var/log/myapp.log"

deploy_app() {
    local app_name="$1"
    local target_host="$2"
    _validate_inputs "$app_name" "$target_host" || return 1
    # ...
}

_validate_inputs() {                # private helper
    [[ -n "$1" ]] && [[ -n "$2" ]]
}

6.10.2 Comment และ Documentation ในสคริปต์

#!/usr/bin/env bash
#===============================================================================
# Name        : deploy.sh
# Description : Deploy application to production servers
# Author      : Moo <moo@example.com>
# Created     : 2026-04-25
# Version     : 2.1.0
#
# Dependencies:
#   - rsync >= 3.1
#   - ssh client
#   - jq for JSON parsing
#
# Exit Codes:
#   0  : สำเร็จ
#   1  : argument ไม่ถูกต้อง
#   2  : config file หาย
#   3  : เชื่อมต่อ remote ไม่ได้
#
# Usage:
#   ./deploy.sh -e ENV -v VERSION [-f]
#
# Examples:
#   ./deploy.sh -e prod -v 2.1.0
#   ./deploy.sh -e staging -v latest -f
#===============================================================================

set -euo pipefail
IFS=$'\n\t'

#-------------------------------------------------------------------------------
# build_artifact: สร้าง artifact จาก source code
# Args:
#   $1 - source directory
#   $2 - output path
# Returns:
#   0 = ทำสำเร็จ
#   1 = source ไม่พบ
#   2 = build ล้มเหลว
# Globals:
#   BUILD_FLAGS (read)
#-------------------------------------------------------------------------------
build_artifact() {
    local src="$1"
    local out="$2"

    [[ -d "$src" ]] || { echo "ERR: source $src not found" >&2; return 1; }
    # ...
}

6.10.3 Idempotency (รันซ้ำได้ผลเหมือนเดิม)

สคริปต์ที่ดี idempotent — รันกี่ครั้งก็ได้ผลเหมือนเดิม ไม่สร้างผลข้างเคียงสะสม เป็นหัวใจของ DevOps automation

#!/usr/bin/env bash

# ❌ ไม่ idempotent — เพิ่ม line ทุกครั้ง
echo "PATH=/opt/myapp/bin:\$PATH" >> ~/.bashrc

# ✅ idempotent — เช็คก่อนเพิ่ม
if ! grep -q "/opt/myapp/bin" ~/.bashrc; then
    echo 'PATH=/opt/myapp/bin:$PATH' >> ~/.bashrc
fi

# ❌ mkdir error ถ้ามีอยู่แล้ว
mkdir /var/log/myapp

# ✅ -p ไม่ error ถ้ามี
mkdir -p /var/log/myapp

# ❌ symlink ซ้ำซ้อน
ln -s /opt/myapp/bin/cli /usr/local/bin/cli

# ✅ ลบก่อนสร้างใหม่
ln -sf /opt/myapp/bin/cli /usr/local/bin/cli

# ❌ เพิ่ม user ทุกครั้ง
useradd -m appuser

# ✅ เช็คก่อน
id -u appuser &>/dev/null || useradd -m appuser

# ❌ rm -rf — error ถ้าไม่มี (ถ้า set -e)
rm -rf /tmp/buildcache

# ✅ rm -rf เป็น idempotent อยู่แล้ว เพราะ -f ignore error
rm -rf /tmp/buildcache  # ปลอดภัย

# ----- pattern: ensure file content -----
ensure_line_in_file() {
    local line="$1"
    local file="$2"
    grep -qxF "$line" "$file" || echo "$line" >> "$file"
}
ensure_line_in_file "deploy.user" /etc/passwd

# ----- pattern: ensure config -----
ensure_config() {
    local key="$1"
    local value="$2"
    local file="$3"

    if grep -q "^${key}=" "$file"; then
        sed -i "s|^${key}=.*|${key}=${value}|" "$file"
    else
        echo "${key}=${value}" >> "$file"
    fi
}
ensure_config "PORT" "8080" /etc/myapp/config
ensure_config "DEBUG" "false" /etc/myapp/config

6.10.4 POSIX-compliant (Portable Script)

ถ้าสคริปต์ต้องรันบนหลายระบบ (Alpine ใช้ ash, FreeBSD ใช้ sh, Embedded ใช้ busybox) ควรเขียนแบบ POSIX

#!/bin/sh
# POSIX-compliant — รันบนทุก /bin/sh ได้

# ❌ Bash-only
arr=(a b c)                       # array ไม่มีใน POSIX
[[ -z "$x" ]]                     # [[ ]] ไม่มี
echo "${var^^}"                   # case manipulation ไม่มี
function foo() {}                 # keyword function ไม่มาตรฐาน

# ✅ POSIX
foo() { echo "ok"; }              # ใช้รูปแบบนี้
[ -z "$x" ] && echo "empty"       # [ ] portable
echo "$var" | tr '[:lower:]' '[:upper:]'   # uppercase

# Array → ใช้ positional parameter
set -- a b c
for item in "$@"; do echo "$item"; done

# String length
len=$(printf '%s' "$var" | wc -c)

# Substring (POSIX)
echo "$var" | cut -c1-5

# ----- ตรวจสอบ POSIX compliance -----
shellcheck --shell=sh script.sh
checkbashisms script.sh           # จาก devscripts package

6.10.5 Style Guide (Google Shell Style Guide)

คำแนะนำหลักจาก Google Shell Style Guide:

  1. เลือกภาษาให้เหมาะ — Shell script ดีสำหรับงาน wrapping commands ถ้าเกิน 100 บรรทัด หรือมี logic ซับซ้อน ใช้ Python/Go ดีกว่า
  2. ใช้ Bash เท่านั้น หากใช้ shell — #!/usr/bin/env bash (ไม่ใช่ csh/ksh/tcsh)
  3. set -euo pipefail เสมอในตอนต้นไฟล์
  4. Indent 2 spaces (ไม่ใช้ tab)
  5. Line length ≤ 80 ตัวอักษร
  6. ใช้ [[ ]] แทน [ ] ใน Bash
  7. Function ต้องอยู่บนสุด ก่อน main code
  8. main() function เป็นจุดเริ่มต้น เรียกท้ายไฟล์ด้วย main "$@"
  9. อย่าใช้ backtick ใช้ $(...) แทน
  10. อย่าใช้ eval ถ้าหลีกเลี่ยงได้
#!/usr/bin/env bash
# Google-style template
set -euo pipefail
IFS=$'\n\t'

# ===== Constants =====
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# ===== Functions =====
err() {
  echo "[$(date +'%F %T')] ERROR: $*" >&2
}

usage() {
  cat <<EOF
Usage: $SCRIPT_NAME [options]
EOF
}

main() {
  if [[ $# -lt 1 ]]; then
    usage
    exit 1
  fi
  # ... ทำงาน
}

# ===== Entry point =====
main "$@"

6.11 ตัวอย่างการประยุกต์ใช้งาน (Practical Applications)

6.11.1 Backup / Restore Script

#!/usr/bin/env bash
#===============================================================================
# Backup Script — สำรอง directory พร้อม timestamp และ rotation
#===============================================================================
set -euo pipefail
IFS=$'\n\t'

readonly SOURCE_DIR="${HOME}"
readonly BACKUP_DIR="/mnt/backup"
readonly KEEP_DAYS=7
readonly LOG_FILE="/var/log/backup.log"

log() {
    printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG_FILE"
}

backup() {
    local timestamp
    timestamp=$(date '+%Y%m%d_%H%M%S')
    local archive="${BACKUP_DIR}/home_${timestamp}.tar.gz"

    log "เริ่มสำรอง $SOURCE_DIR$archive"

    # สร้างปลายทางถ้ายังไม่มี
    mkdir -p "$BACKUP_DIR"

    # คำนวณขนาด source
    local size
    size=$(du -sh "$SOURCE_DIR" | cut -f1)
    log "ขนาด source: $size"

    # สำรองด้วย tar
    if tar --exclude='.cache' \
           --exclude='node_modules' \
           --exclude='.venv' \
           --exclude='*.pyc' \
           -czf "$archive" \
           -C "$(dirname "$SOURCE_DIR")" \
           "$(basename "$SOURCE_DIR")" 2>>"$LOG_FILE"; then
        local arc_size
        arc_size=$(du -h "$archive" | cut -f1)
        log "สำเร็จ: $archive ($arc_size)"
    else
        log "ล้มเหลว: tar exit $?"
        return 1
    fi
}

rotate() {
    log "ลบ backup เก่ากว่า $KEEP_DAYS วัน"
    find "$BACKUP_DIR" -name "home_*.tar.gz" -mtime "+$KEEP_DAYS" \
        -print -delete | tee -a "$LOG_FILE"
}

restore() {
    local archive="$1"
    local target="${2:-$HOME.restored}"

    [[ -f "$archive" ]] || { log "ERROR: ไม่พบ $archive"; return 1; }

    log "กู้คืน $archive$target"
    mkdir -p "$target"
    tar -xzf "$archive" -C "$target"
    log "กู้คืนสำเร็จ"
}

main() {
    case "${1:-backup}" in
        backup)  backup; rotate ;;
        restore) restore "${2:-}" "${3:-}" ;;
        list)    ls -lh "$BACKUP_DIR" ;;
        *)       echo "Usage: $0 {backup|restore <file>|list}"; exit 1 ;;
    esac
}

main "$@"

6.11.2 Log Rotation และ Cleanup

#!/usr/bin/env bash
#===============================================================================
# Log Rotation Script — บีบอัดและหมุนเวียน log
#===============================================================================
set -euo pipefail

readonly LOG_DIR="/var/log/myapp"
readonly MAX_SIZE_MB=100        # เกินนี้จะหมุน
readonly MAX_KEEP=10            # เก็บไม่เกินนี้
readonly COMPRESS_AFTER=1       # บีบอัดหลังหมุน N วัน

rotate_log() {
    local logfile="$1"
    local size_mb
    size_mb=$(( $(stat -c%s "$logfile") / 1024 / 1024 ))

    if (( size_mb < MAX_SIZE_MB )); then
        echo "Skip: $logfile (${size_mb}MB < ${MAX_SIZE_MB}MB)"
        return
    fi

    echo "Rotating $logfile (${size_mb}MB)"

    # หมุนหมายเลข — file.9.gz → ลบ, file.8.gz → file.9.gz, ...
    for ((i = MAX_KEEP - 1; i >= 1; i--)); do
        local cur="${logfile}.${i}"
        local nxt="${logfile}.$((i + 1))"
        [[ -f "${cur}.gz" ]] && mv "${cur}.gz" "${nxt}.gz"
        [[ -f "${cur}" ]] && mv "${cur}" "${nxt}"
    done

    # current → .1
    mv "$logfile" "${logfile}.1"

    # สร้าง log ว่างใหม่
    : > "$logfile"
    chmod 644 "$logfile"

    # ส่ง SIGHUP ให้แอป (ถ้าจำเป็น)
    # systemctl reload myapp
}

compress_old() {
    find "$LOG_DIR" -name "*.[0-9]" \
         -mtime "+$COMPRESS_AFTER" \
         -exec gzip {} \;
}

cleanup_old() {
    find "$LOG_DIR" -name "*.[0-9].gz" \
         -mtime "+$((MAX_KEEP * 7))" \
         -delete
}

main() {
    [[ -d "$LOG_DIR" ]] || { echo "ไม่พบ $LOG_DIR"; exit 1; }

    for logfile in "$LOG_DIR"/*.log; do
        [[ -f "$logfile" ]] || continue
        rotate_log "$logfile"
    done

    compress_old
    cleanup_old

    echo "Log rotation เสร็จสิ้น"
}

main "$@"

6.11.3 System Monitoring (CPU, RAM, Disk)

#!/usr/bin/env bash
#===============================================================================
# System Monitor — ตรวจสอบและแจ้งเตือนเมื่อทรัพยากรเกิน threshold
#===============================================================================
set -euo pipefail

readonly CPU_THRESHOLD=80
readonly RAM_THRESHOLD=85
readonly DISK_THRESHOLD=90
readonly ALERT_FILE="/tmp/sysmon_alert"

check_cpu() {
    local cpu_usage
    # คำนวณ % CPU จาก idle
    cpu_usage=$(top -bn1 | awk '/Cpu\(s\)/ {print 100 - $8}' | cut -d. -f1)

    printf 'CPU  : %s%%\n' "$cpu_usage"

    if (( cpu_usage > CPU_THRESHOLD )); then
        alert "CPU สูงเกิน: ${cpu_usage}%"
    fi
}

check_ram() {
    local total used percent
    read -r total used <<< "$(free -m | awk '/^Mem:/ {print $2, $3}')"
    percent=$(( used * 100 / total ))

    printf 'RAM  : %dMB / %dMB (%d%%)\n' "$used" "$total" "$percent"

    if (( percent > RAM_THRESHOLD )); then
        alert "RAM สูงเกิน: ${percent}%"
    fi
}

check_disk() {
    while read -r fs size used avail percent mount; do
        # ตัด % ออก
        local pct="${percent%\%}"
        printf 'Disk : %s %s/%s (%s)\n' "$mount" "$used" "$size" "$percent"

        if (( pct > DISK_THRESHOLD )); then
            alert "Disk $mount สูงเกิน: ${percent}"
        fi
    done < <(df -h --output=source,size,used,avail,pcent,target \
             | grep -E '^/dev/' | tail -n +1)
}

check_load() {
    local load1
    load1=$(awk '{print $1}' /proc/loadavg)
    local cores
    cores=$(nproc)
    printf 'Load : %s (cores=%d)\n' "$load1" "$cores"

    # alert ถ้า load > จำนวน core
    if awk -v l="$load1" -v c="$cores" 'BEGIN { exit !(l > c) }'; then
        alert "Load average สูง: $load1 (cores=$cores)"
    fi
}

alert() {
    local msg="$1"
    local timestamp
    timestamp=$(date '+%F %T')
    echo "[$timestamp] ALERT: $msg" | tee -a "$ALERT_FILE" >&2

    # ส่ง notification (แล้วแต่ระบบ)
    if command -v notify-send &>/dev/null; then
        notify-send -u critical "System Alert" "$msg"
    fi
    # หรือส่ง email/Slack/Telegram
    # echo "$msg" | mail -s "System Alert" admin@example.com
}

print_header() {
    echo "===== System Monitor: $(hostname) ====="
    echo "Time : $(date '+%F %T')"
    echo "Up   : $(uptime -p)"
    echo "----------------------------------------"
}

main() {
    print_header
    check_cpu
    check_ram
    check_disk
    check_load
    echo "========================================"
}

main "$@"

6.11.4 Batch File Processing

#!/usr/bin/env bash
#===============================================================================
# Batch Image Converter — แปลงรูปภาพหลายไฟล์เป็น webp ขนาดเล็ก
#===============================================================================
set -euo pipefail

readonly INPUT_DIR="${1:-./images}"
readonly OUTPUT_DIR="${2:-./output}"
readonly QUALITY=85
readonly MAX_WIDTH=1920
readonly PARALLEL_JOBS=4

# ตรวจสอบ dependencies
check_deps() {
    for cmd in convert identify cwebp; do
        if ! command -v "$cmd" &>/dev/null; then
            echo "Error: ต้องการ $cmd (ติดตั้ง imagemagick + webp)" >&2
            return 1
        fi
    done
}

# แปลง 1 ไฟล์
convert_image() {
    local input="$1"
    local relative="${input#$INPUT_DIR/}"
    local output="$OUTPUT_DIR/${relative%.*}.webp"

    mkdir -p "$(dirname "$output")"

    # ถ้า output มีอยู่และใหม่กว่า ก็ข้าม (idempotent)
    if [[ -f "$output" && "$output" -nt "$input" ]]; then
        echo "skip: $relative"
        return
    fi

    local width
    width=$(identify -format '%w' "$input")

    if (( width > MAX_WIDTH )); then
        # resize + convert
        convert "$input" -resize "${MAX_WIDTH}>" -strip -quality $QUALITY \
                "$output"
    else
        cwebp -q $QUALITY "$input" -o "$output" -quiet
    fi

    local in_size out_size
    in_size=$(stat -c%s "$input")
    out_size=$(stat -c%s "$output")
    local saved=$(( (in_size - out_size) * 100 / in_size ))
    printf '%-40s %3d%% saved\n' "$relative" "$saved"
}

export -f convert_image
export INPUT_DIR OUTPUT_DIR QUALITY MAX_WIDTH

main() {
    check_deps

    [[ -d "$INPUT_DIR" ]] || { echo "ไม่พบ $INPUT_DIR"; exit 1; }
    mkdir -p "$OUTPUT_DIR"

    echo "แปลงรูปจาก $INPUT_DIR$OUTPUT_DIR"
    echo "ใช้ $PARALLEL_JOBS jobs ขนาน"

    # หาไฟล์ภาพและ parallel ด้วย xargs
    find "$INPUT_DIR" -type f \
         \( -iname '*.jpg' -o -iname '*.jpeg' \
            -o -iname '*.png' -o -iname '*.bmp' \) \
         -print0 \
        | xargs -0 -n1 -P "$PARALLEL_JOBS" \
                bash -c 'convert_image "$1"' _

    echo "เสร็จสิ้น"
}

main "$@"

6.11.5 Automation ร่วมกับ cron, systemd timer

วิธี 1: cron

# แก้ crontab ของผู้ใช้ปัจจุบัน
crontab -e

# format: m h dom mon dow command
# ตัวอย่าง crontab:

# ทุกวันเที่ยงคืน — สำรองข้อมูล
0 0 * * * /opt/scripts/backup.sh >>/var/log/backup.log 2>&1

# ทุก 5 นาที — ตรวจสอบ system
*/5 * * * * /opt/scripts/sysmon.sh

# ทุกชั่วโมงในเวลาทำงาน
0 9-17 * * 1-5 /opt/scripts/work-hours-check.sh

# ทุกอาทิตย์ตี 3 — log rotation
0 3 * * 0 /opt/scripts/log-rotate.sh

# วันที่ 1 ของเดือน — รายงาน
0 6 1 * * /opt/scripts/monthly-report.sh

# ----- ตัวอย่างใส่ environment -----
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO="admin@example.com"
0 2 * * * /opt/scripts/backup.sh

วิธี 2: systemd timer (แนะนำกว่า cron บน Linux ปัจจุบัน)

# /etc/systemd/system/myapp-backup.service
cat > /etc/systemd/system/myapp-backup.service <<'EOF'
[Unit]
Description=MyApp Backup Service
After=network.target

[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup.sh
StandardOutput=journal
StandardError=journal
EOF

# /etc/systemd/system/myapp-backup.timer
cat > /etc/systemd/system/myapp-backup.timer <<'EOF'
[Unit]
Description=Run MyApp Backup Daily at 2 AM
Requires=myapp-backup.service

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
EOF

# enable และ start
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-backup.timer

# ตรวจสอบ
systemctl list-timers --all
systemctl status myapp-backup.timer
journalctl -u myapp-backup.service -f

6.11.6 Deployment Script

#!/usr/bin/env bash
#===============================================================================
# Deployment Script — deploy แอปไปยัง server พร้อม rollback
#===============================================================================
set -euo pipefail
IFS=$'\n\t'

readonly APP_NAME="myapp"
readonly DEPLOY_USER="deploy"
readonly REMOTE_HOST="${REMOTE_HOST:-prod.example.com}"
readonly REMOTE_DIR="/opt/${APP_NAME}"
readonly RELEASES_DIR="${REMOTE_DIR}/releases"
readonly CURRENT_LINK="${REMOTE_DIR}/current"
readonly KEEP_RELEASES=5

log() { printf '[%s] %s\n' "$(date +%T)" "$*"; }
err() { printf '[%s] ERROR: %s\n' "$(date +%T)" "$*" >&2; }

# ตรวจสอบ prerequisites
preflight() {
    log "ตรวจสอบ prerequisites"
    command -v rsync &>/dev/null || { err "ต้องการ rsync"; exit 1; }
    command -v ssh   &>/dev/null || { err "ต้องการ ssh";   exit 1; }
    [[ -d ./dist ]]               || { err "ไม่มี ./dist"; exit 1; }

    # ทดสอบเชื่อมต่อ
    if ! ssh -o ConnectTimeout=5 "${DEPLOY_USER}@${REMOTE_HOST}" true; then
        err "เชื่อมต่อ ${REMOTE_HOST} ไม่ได้"
        exit 1
    fi
    log "OK"
}

# build artifact
build() {
    log "Build artifact"
    npm ci
    npm run build
    log "Build เสร็จสิ้น"
}

# upload ไปยัง server
upload() {
    local release_id
    release_id=$(date +%Y%m%d_%H%M%S)
    local release_path="${RELEASES_DIR}/${release_id}"

    log "Upload → ${release_path}"

    ssh "${DEPLOY_USER}@${REMOTE_HOST}" "mkdir -p '${release_path}'"

    rsync -avz --delete \
          --exclude='node_modules' \
          --exclude='.git' \
          ./dist/ \
          "${DEPLOY_USER}@${REMOTE_HOST}:${release_path}/"

    echo "$release_id"
}

# สลับ symlink และรีสตาร์ท
activate() {
    local release_id="$1"
    local release_path="${RELEASES_DIR}/${release_id}"

    log "Activate ${release_id}"

    ssh "${DEPLOY_USER}@${REMOTE_HOST}" bash <<EOF
set -euo pipefail
ln -sfn '${release_path}' '${CURRENT_LINK}'
sudo systemctl restart ${APP_NAME}.service
sleep 2
sudo systemctl is-active ${APP_NAME}.service
EOF
    log "Active แล้ว"
}

# health check
health_check() {
    log "Health check"
    local i=0
    while (( i < 30 )); do
        if curl -sf "http://${REMOTE_HOST}/health" &>/dev/null; then
            log "Healthy ✓"
            return 0
        fi
        sleep 2
        ((i++))
    done
    err "Health check fail หลัง 60s"
    return 1
}

# ลบ release เก่า
cleanup() {
    log "ลบ release เก่า เหลือไว้ ${KEEP_RELEASES} ตัว"
    ssh "${DEPLOY_USER}@${REMOTE_HOST}" bash <<EOF
cd '${RELEASES_DIR}'
ls -t | tail -n +$((KEEP_RELEASES + 1)) | xargs -r rm -rf
EOF
}

# rollback
rollback() {
    log "Rollback ไปยัง release ก่อนหน้า"
    ssh "${DEPLOY_USER}@${REMOTE_HOST}" bash <<EOF
set -e
cd '${RELEASES_DIR}'
prev=\$(ls -t | sed -n '2p')
[[ -n "\$prev" ]] || { echo "ไม่มี previous release"; exit 1; }
ln -sfn '${RELEASES_DIR}/'"\$prev" '${CURRENT_LINK}'
sudo systemctl restart ${APP_NAME}.service
echo "Rollback ไป \$prev"
EOF
}

main() {
    case "${1:-deploy}" in
        deploy)
            preflight
            build
            local rid
            rid=$(upload)
            activate "$rid"
            if health_check; then
                cleanup
                log "✓ Deploy ${rid} สำเร็จ"
            else
                err "✗ Health check fail — ทำการ rollback"
                rollback
                exit 1
            fi
            ;;
        rollback) rollback ;;
        list)
            ssh "${DEPLOY_USER}@${REMOTE_HOST}" \
                "ls -lt '${RELEASES_DIR}'"
            ;;
        *)
            cat <<EOF
Usage: $0 {deploy|rollback|list}
  deploy   - build, upload, activate ใหม่
  rollback - ย้อนกลับไป release ก่อนหน้า
  list     - แสดง release ทั้งหมด
EOF
            exit 1
            ;;
    esac
}

main "$@"
flowchart TD
    A[Start deploy] --> B[Preflight
ตรวจ deps + connection] B --> C[Build artifact
npm/cargo/go build] C --> D[Upload via rsync
ไปยัง releases/timestamp] D --> E[Activate
สลับ symlink current] E --> F[Restart service
systemctl restart] F --> G{Health check
200 OK?} G -->|Pass| H[Cleanup old
ลบ release เก่า] G -->|Fail| I[Rollback
previous release] H --> J[Done ✓] I --> K[Exit 1 ✗]

สรุปท้ายบท (Chapter Summary)

Shell Script เป็นเครื่องมือสำคัญที่นักพัฒนาและผู้ดูแลระบบ Linux ทุกคนต้องใช้ทุกวัน บทนี้ได้กล่าวถึงตั้งแต่พื้นฐาน — โครงสร้างไฟล์ ตัวแปร เงื่อนไข การวนซ้ำ ฟังก์ชัน — ไปจนถึงเทคนิคขั้นสูงเช่น associative array, here document, getopts และ trap signal สิ่งที่สำคัญที่สุดคือ การเขียนสคริปต์ที่ปลอดภัย ผ่าน set -euo pipefail, การใช้ local ในฟังก์ชัน, การ quote ตัวแปรเสมอ, และการตรวจ syntax ด้วย ShellCheck

ตัวอย่างประยุกต์ในตอนท้ายแสดงให้เห็นว่าสคริปต์ที่เขียนอย่างเป็นระบบสามารถทำงานสำคัญได้จริง ตั้งแต่ backup, monitoring, batch processing, ไปจนถึง deployment ที่มี rollback การฝึกฝนอย่างต่อเนื่องและการอ่านสคริปต์ของผู้อื่น (เช่นใน /etc/init.d/, ~/.bashrc, GitHub) จะช่วยให้พัฒนาทักษะการเขียน Shell Script ได้อย่างรวดเร็ว

แหล่งศึกษาเพิ่มเติม: