POSIX: Shell Script

การเขียน 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 ที่ไม่ใช่ 0
  • set -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 Outputprintf "%s\n" "text"echo -e "text\n"
Function Definemy_func() { ... }function my_func() { ... }
Arrayเก็บในลิสต์ข้อความ หรือ $@arr=(a b c)
Brace Expansionเขียนแจกแจงตรงๆ หรือใช้ seqecho {1..10}, {a,b,c}
Source Script. ./other_script.shsource ./other_script.sh
Process Substitutionใช้ Named Pipe (mkfifo) หรือ Temp file<(command)

โครงสร้างเทมเพลตสคริปต์มาตรฐาน POSIX

#!/bin/sh
set -eu

# ฟังก์ชันแสดง Help
usage() {
    printf "Usage: %s [-h] [-e env] &lt;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" >&amp;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 ปนเปื้อน