การเขียน Shell Script ให้เป็นไปตามมาตรฐาน POSIX (POSIX-compliant) มีเป้าหมายหลักเพื่อให้สคริปต์สามารถรันได้บนทุกระบบปฏิบัติการตระกูล Unix-like ตั้งแต่ Linux distributions ต่างๆ, macOS, FreeBSD ไปจนถึงสภาพแวดล้อมขนาดเล็กอย่าง Alpine Linux (BusyBox ash) หรือคอนเทนเนอร์ Docker โดยไม่พึ่งพาฟีเจอร์เฉพาะของ Bash (Bashisms)
กฎพื้นฐาน 7 ประการในการหลีกเลี่ยง Bashisms
1. Shebang ต้องชี้ไปที่ /bin/sh
อย่าใช้ #!/bin/bash เพราะในบางระบบ เช่น FreeBSD หรือ Alpine อาจไม่มี Bash ติดตั้งมาแต่แรก
#!/bin/sh
set -eu
set -e: สั่งให้ออกจากสคริปต์ทันทีเมื่อคำสั่งใดคำสั่งหนึ่งส่งคืน exit status ที่ไม่ใช่ 0set -u: ป้องกันการเรียกใช้ตัวแปรที่ยังไม่ได้ประกาศ (Unset variables)
2. ใช้ [ (test) แทน [[
[[ ... ]] เป็นไวยากรณ์เฉพาะของ Bash/Zsh/Ksh เท่านั้น ใน POSIX ต้องใช้ [ หรือคำสั่ง test และใช้เครื่องมือเปรียบเทียบมาตรฐาน
# แบบ POSIX (ถูกต้อง)
if [ "$env_name" = "production" ] && [ -f "$config_file" ]; then
echo "Running in production"
fi
# ห้ามใช้ (Bashism):
# if [[ $env_name == "production" && -f $config_file ]]; then ...
- สำหรับการเปรียบเทียบ String ให้ใช้
=เพียงตัวเดียว ไม่ใช่== - รวมเงื่อนไขด้วย
[ ... ] && [ ... ]ไม่ใช้-aหรือ-o(เนื่องจากพฤติกรรมในบางระบบอาจกำกวมตามข้อกำหนด POSIX)
3. การประกาศฟังก์ชัน (Function Declaration)
POSIX ไม่รองรับคีย์เวิร์ด function ให้ใช้รูปแบบ name() โดยตรง และหลีกเลี่ยงการใช้คำสั่ง local
# แบบ POSIX (ถูกต้อง)
deploy_app() {
target_dir="$1"
echo "Deploying to $target_dir"
}
# ห้ามใช้ (Bashism):
# function deploy_app { local target_dir="$1"; ... }
หมายเหตุเรื่อง Scope ตัวแปร: คำสั่ง
localไม่มีในสเปก POSIX (แม้เชลล์หลายตัวจะรับได้) หากต้องการแยกขอบเขตตัวแปรอย่างเข้มงวด ควรใช้ Subshell ด้วยวงเล็บโค้งisolate_scope() ( temp_var="temporary" echo "$temp_var" )
4. การคำนวณคณิตศาสตร์ (Arithmetic Expansion)
ใช้ $(( ... )) สำหรับการคำนวณจำนวนเต็ม ไม่ใช้ let หรือ $[ ... ]
count=1
count=$((count + 1))
total=$(((count * 2) + 5))
5. การรันคำสั่งย่อย (Command Substitution)
ใช้ $(command) แทน backticks (`command`) เนื่องจากอ่านง่ายกว่าและสามารถซ้อนกัน (Nesting) ได้โดยไม่มีปัญหา Escape
# แบบ POSIX (ถูกต้อง)
current_user="$(whoami)"
app_files="$(find "$target_dir" -type f)"
# ไม่แนะนำ:
# current_user=`whoami`
6. การแสดงผลด้วย printf แทน echo
คำสั่ง echo มีความแตกต่างของ flag สูงมากในแต่ละระบบ (เช่น echo -n หรือ echo -e ไม่ทำงานบนบางเชลล์) POSIX แนะนำให้ใช้ printf เสมอ
# แบบ POSIX (ถูกต้องและคาดเดาผลลัพธ์ได้ 100%)
printf "Processing item: %s\n" "$item_name"
printf "Prompt: " # ไม่ขึ้นบรรทัดใหม่ แทน echo -n
# ห้ามใช้ (ไม่สม่ำเสมอ):
# echo -n "Prompt: "
# echo -e "Line 1\nLine 2"
7. หลีกเลี่ยง Arrays และการขยาย String ขั้นสูง
POSIX sh ไม่มี Array (มีเพียง positional parameters $1, $2, …) และไม่รองรับ Parameter Slices
# หากต้องการวนลูปรายการข้อมูล ให้ใช้ Space-separated string หรือ Positional parameters
servers="srv1 srv2 srv3"
for srv in $servers; do
printf "Connecting to: %s\n" "$srv"
done
# ห้ามใช้ (Bashism):
# server_list=("srv1" "srv2" "srv3")
# echo "${server_list[0]}"
# echo "${var:0:4}"
ตารางเปรียบเทียบไวยากรณ์: POSIX sh vs Bash
| ฟังก์ชันการทำงาน | POSIX Compliant (/bin/sh) | Bashism (/bin/bash) |
| Condition Test | [ "$a" = "$b" ] | [[ $a == $b ]] |
| Regex Match | ใช้ expr หรือ grep -E | [[ $str =~ $pattern ]] |
| Print Output | printf "%s\n" "text" | echo -e "text\n" |
| Function Define | my_func() { ... } | function my_func() { ... } |
| Array | เก็บในลิสต์ข้อความ หรือ $@ | arr=(a b c) |
| Brace Expansion | เขียนแจกแจงตรงๆ หรือใช้ seq | echo {1..10}, {a,b,c} |
| Source Script | . ./other_script.sh | source ./other_script.sh |
| Process Substitution | ใช้ Named Pipe (mkfifo) หรือ Temp file | <(command) |
โครงสร้างเทมเพลตสคริปต์มาตรฐาน POSIX
#!/bin/sh
set -eu
# ฟังก์ชันแสดง Help
usage() {
printf "Usage: %s [-h] [-e env] <target>\n" "$0"
exit 1
}
env="production"
# ประมวลผล Flag arguments ด้วย getopts (POSIX Standard)
while getopts "he:" opt; do
case "$opt" in
h) usage ;;
e) env="$OPTARG" ;;
*) usage ;;
esac
done
shift "$((OPTIND - 1))"
# ตรวจสอบ positional argument ที่จำเป็น
if [ "$#" -lt 1 ]; then
printf "Error: Missing target directory.\n\n" >&2
usage
fi
target_path="$1"
# ตัวอย่างการทำงาน
printf "Target: %s\n" "$target_path"
printf "Environment: %s\n" "$env"
if [ -d "$target_path" ]; then
printf "Directory exists. Proceeding...\n"
else
printf "Creating directory: %s\n" "$target_path"
mkdir -p "$target_path"
fi
เครื่องมือตรวจสอบความถูกต้อง (Linting)
ก่อนนำสคริปต์ไปใช้งานจริง ควรตรวจสอบด้วยเครื่องมืออัตโนมัติ:
- ShellCheck
สามารถรันผ่าน CLI หรือใส่ในขั้นตอน CI/CD:Bashshellcheck -s sh my_script.sh(ระบุ-s shเพื่อให้ ShellCheck ตรวจจับคำสั่งที่เป็น Bashism ทันที) - ทดสอบรันด้วยเชลล์ขนาดเล็ก
ทดสอบรันด้วยdash(Debian Almquist Shell) หรือbusybox shเพื่อยืนยันว่าสคริปต์ทำงานได้จริงโดยไม่มี Bash ปนเปื้อน