#!/usr/bin/env sh # lint-scripts.sh — 檢查一個 domain 存取庫的 shell 腳本:語法、可執行、結束碼有沒有記載。 # # 用法: lint-scripts.sh # # 檢查三項(掃 {domain-path}/tools 與 {domain-path}/hooks 底下的 *.sh): # 1. 語法 — sh -n 通過。 # 2. 可執行 — 檔案有執行權限。技能直接呼叫的腳本沒有 +x,會在部署後才炸。 # 3. 結束碼 — 檔頭前 60 行要有結束碼宣告列(「結束碼:」或「exit code」)。 # 沒有宣告的腳本,呼叫端沒辦法逐碼分流,只能猜;猜錯就把失敗當成功。 # # 為什麼合成一支: skill-check 原本在 SKILL.md 裡寫 find ... -exec sh -n,另外兩項靠散文 # 要求人工比對。三項的輸入輸出都固定,散文版每個 domain 各做一次,還會跟腳本實況漂移。 # # 例外: 只被 source 的共用函式庫(判準見 is_library)第 2、3 項都不查——它不是可執行入口, # 沒有 +x 的必要,也沒有結束碼語意可宣告。第 1 項語法檢查照查,函式庫一樣會被 sh -n 讀。 # 豁免不是靜默的: 每豁免一支就在 stderr 記一行,摘要也帶函式庫支數,才看得出誰被跳過。 # # 輸出: 一行一個不合格項目,格式 {檔案}:{檢查項}:{說明}(stdout); # 函式庫豁免通知與統計摘要走 stderr。 # 結束碼: 0=掃到腳本且三項全過 # 1=有不合格項目(清單在 stdout) # 2=用法錯誤(本腳本只吃一個參數) # 3=domain 路徑不存在,或 tools/ 與 hooks/ 都沒有 *.sh——**什麼都沒掃**, # 不等於通過。缺這兩個目錄本身不是失敗,呼叫端照實記「無腳本可掃」即可。 set -u usage() { echo 'usage: lint-scripts.sh ' >&2 exit 2 } [ "$#" -eq 1 ] || usage DOMAIN=${1%/} [ -n "$DOMAIN" ] || usage [ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; } SCAN='' for d in "$DOMAIN/tools" "$DOMAIN/hooks"; do [ -d "$d" ] && SCAN="$SCAN $d" done [ -n "$SCAN" ] || { echo "沒有 tools/ 也沒有 hooks/,無腳本可掃:$DOMAIN" >&2; exit 3; } TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; } trap 'rm -f "$TMP"' EXIT # shellcheck disable=SC2086 find $SCAN -type f -name '*.sh' 2>/dev/null | sort > "$TMP" [ -s "$TMP" ] || { echo "tools/ 與 hooks/ 底下沒有 *.sh,無腳本可掃:$DOMAIN" >&2; exit 3; } has_exit_stmt() { # $1=檔案;去掉整行註解後還找得到 exit 敘述就回 0 grep -v '^[[:space:]]*#' "$1" 2>/dev/null \ | grep -qE '(^|[;&|(){}[:space:]])exit([[:space:]]|$)' } is_library() { # $1=檔案;只被 source 的共用函式庫,可執行與結束碼兩項都豁免 # 判準兩條,缺一不可: # 1. 自我宣告是函式庫——檔名 lib.sh,或檔頭前 30 行寫了「以 source 載入」。 # 2. 程式碼裡一個 exit 敘述都沒有。沒有 exit 就沒有結束碼語意,也就沒有東西可宣告; # 這一條同時是「不是可執行入口」的證據,兩項豁免共用同一個事實。 # 為什麼不能只看第 1 條: 那是字樣比對,會命中「提到這件事」的腳本。本腳本自己的 # 例外段就寫了「以 source 載入」,plugins-root.sh 的用法段也寫了,但兩支都是有 # 結束碼語意的可執行入口。只拿第 1 條去豁免結束碼,等於把工具自己漏掉。 case "${1##*/}" in lib.sh) ;; *) head -30 "$1" 2>/dev/null | grep -q '以 source 載入' || return 1 ;; esac ! has_exit_stmt "$1" } hit=0 total=0 libs=0 while IFS= read -r f; do [ -n "$f" ] || continue total=$((total + 1)) err=$(sh -n "$f" 2>&1) || { printf '%s:語法:%s\n' "$f" "$(printf '%s' "$err" | tr '\n' ' ')" hit=1 } if is_library "$f"; then libs=$((libs + 1)) printf '%s:函式庫:判定為只被 source 的共用函式庫(無 exit 敘述),略過「可執行」與「結束碼」\n' "$f" >&2 continue fi if [ ! -x "$f" ]; then printf '%s:可執行:缺執行權限,請 chmod +x\n' "$f" hit=1 fi if ! head -60 "$f" 2>/dev/null | grep -qE '結束碼|exit code'; then printf '%s:結束碼:檔頭沒有結束碼宣告列,呼叫端無法逐碼分流\n' "$f" hit=1 fi done < "$TMP" if [ "$hit" -eq 0 ]; then echo "腳本檢查通過:$total 支(語法、可執行、結束碼宣告),其中 $libs 支判定為函式庫" >&2 else echo "腳本檢查有不合格項目:共掃 $total 支(其中 $libs 支判定為函式庫),清單見 stdout" >&2 fi exit $hit