PlusMagi's Blog By Pitt Phunsanit Shell Script เขียน Script ตรวจจับและ Lint ไฟล์ที่ไม่มีนามสกุล .sh

เขียน Script ตรวจจับและ Lint ไฟล์ที่ไม่มีนามสกุล .sh

สคริปต์นี้เขียนขึ้นตามมาตรฐาน POSIX shell (/bin/sh) โดยใช้วิธีอ่านบรรทัดแรกของไฟล์ (First Line Header) เพื่อดักจับ Shebang ที่ชี้ไปยัง Shell ต่างๆ (เช่น sh, bash, dash, ash, zsh หรือ /usr/bin/env sh) แม้ว่าไฟล์นั้นจะไม่มีนามสกุล .sh เช่นไฟล์ในไดเรกทอรี bin/ หรือ CLI tools


สคริปต์ lint-all-shells.sh

#!/bin/sh
set -eu

# ตรวจสอบว่ามีคำสั่ง shellcheck ติดตั้งอยู่ในระบบหรือไม่
if ! command -v shellcheck >/dev/null 2>&1; then
    printf "Error: 'shellcheck' command not found. Please install it first.\n" >&2
    exit 1
fi

target_dir="${1:-.}"
failed_count=0
checked_count=0

printf "Scanning directory: %s\n\n" "$target_dir"

# ใช้ find ค้นหาไฟล์ปกติ โดยข้ามโฟลเดอร์ที่ไม่เกี่ยวข้อง เช่น .git, vendor, node_modules
# จัดการผ่าน While-loop เพื่อความเข้ากันได้กับ POSIX
find "$target_dir" \
    -type d \( -name ".git" -o -name "vendor" -o -name "node_modules" -o -name ".build" \) -prune \
    -o -type f -print | while IFS= read -r file; do

    # ข้ามไฟล์เปล่า (ขนาด 0 ไบต์) หรือไฟล์ที่ไม่มีสิทธิ์อ่าน
    [ -s "$file" ] && [ -r "$file" ] || continue

    is_shell_script=0

    # 1. ตรวจสอบจากนามสกุลไฟล์
    case "$file" in
        *.sh|*.bash|*.ksh)
            is_shell_script=1
            ;;
        *)
            # 2. กรณีไม่มีนามสกุล: อ่านเฉพาะบรรทัดแรกสุดเพื่อตรวจจับ Shebang
            first_line=""
            IFS= read -r first_line < "$file" || [ -n "$first_line" ]

            case "$first_line" in
                '#!'*sh*)
                    # กรองเฉพาะ Shebang ที่มีคำว่า sh, bash, zsh, ksh, ash, dash
                    case "$first_line" in
                        *'/bin/'*sh*|*'/env '*sh*|*'/env '*bash*|*'/env '*zsh*)
                            is_shell_script=1
                            ;;
                    esac
                    ;;
            esac
            ;;
    esac

    # ถ้ายืนยันว่าเป็น Shell Script ให้ส่งเข้า ShellCheck
    if [ "$is_shell_script" -eq 1 ]; then
        printf "==> Linting: %s\n" "$file"
        if ! shellcheck "$file"; then
            # แจ้งเตือนเมื่อพบข้อผิดพลาด
            printf "FAILED: %s\n\n" "$file" >&2
            exit 1
        fi
        printf "PASSED\n\n"
    fi
done

printf "Linting completed successfully.\n"


วิธีนำไปใช้งาน

1. ให้สิทธิ์ Execute แก่ไฟล์

chmod +x lint-all-shells.sh

2. รันสแกนทั้งโปรเจกต์

# ตรวจสอบไดเรกทอรีปัจจุบัน
./lint-all-shells.sh

# หรือระบุพาธเป้าหมาย
./lint-all-shells.sh ./bin


จุดเด่นและการทำงานเชิงลึก

  • ตรวจจับทั้งแบบมีและไม่มี Extension
    รองรับทั้ง deploy.sh, script.bash และไฟล์ไบนารีสคริปต์ เช่น bin/my-tool ที่มีบรรทัดแรกเป็น #!/usr/bin/env bash หรือ #!/bin/sh
  • ประหยัด I/O และหน่วยความจำ
    ใช้คำสั่ง read -r first_line < "$file" อ่านเฉพาะบรรทัดแรกสุดของแต่ละไฟล์ ไม่โหลดไฟล์ขนาดใหญ่ทั้งหมดลงใน Memory
  • POSIX Compliant
    ใช้ไวยากรณ์ case ... in ในการ Match ข้อความและ Wildcard แทนการเรียกคำสั่งภายนอกอย่าง grep หรือ Bash Regex (=~) ทำให้ทำงานได้เร็วและรันได้บนทุกระบบ
  • ข้ามโฟลเดอร์ที่ไม่จำเป็น
    ตัดพาธ .git, node_modules, vendor ออกตั้งแต่ระดับ find -prune เพื่อลดเวลาสแกนในโปรเจกต์ขนาดใหญ่