Shell 腳本中的模組化設計 文章首圖

Shell 腳本中的模組化設計

Shell 腳本中的模組化設計

在初學 Shell 腳本時,我們往往習慣將所有邏輯寫在單一檔案中,隨著功能增加,腳本會變得冗長且難以維護。這就像是在一個巨大的義大利麵(Spaghetti Code)中尋找特定邏輯一樣困難。對於經常處理系統管理、自動化部署或 CI/CD 腳本的工程師來說,模組化設計(Modular Design) 不僅是最佳實踐,更是確保腳本可讀性、可測試性與可重用性的關鍵。

本文將探討如何在 Bash 中實現模組化,並提供實際可執行的範例。

為什麼需要模組化?

模組化的核心在於「關注點分離」(Separation of Concerns)。透過將功能拆分為獨立的功能區塊或檔案,我們可以達到以下好處:

  1. 避免命名衝突:不同模組中的函數名稱不會互相干擾。
  2. 提升可讀性:主腳本只需關注流程控制,細節邏輯隱藏在模組中。
  3. 方便除錯與測試:單一函數可獨立測試,無需執行整個腳本。
  4. 重複使用:將通用工具函數提取為庫,供其他腳本呼叫。

方法一:使用 source. 指令載入模組

在 Bash 中,最常見的模組化方式是將共用函數存放在單獨的檔案中,然後在主腳本中使用 source. 指令將其載入。這會將模組的內容直接注入到當前腳本的執行環境中。

1. 建立模組檔案

首先,我們建立一個名為 utils.sh 的檔案,其中包含一些常用的工具函數。請注意,模組檔案中通常只包含函數定義,不應包含直接執行的邏輯或變數賦值(除非是設定檔)。

# utils.sh
#!/bin/bash

# 檢查命令是否存在
check_command() {
    if ! command -v "$1" &> /dev/null; then
        echo "錯誤: 找不到命令 '$1',請先安裝。" >&2
        return 1
    fi
    return 0
}

# 記錄日誌
log_info() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1"
}

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" >&2
}

# 計算兩個數字的和
add_numbers() {
    local num1=$1
    local num2=$2
    echo $(( num1 + num2 ))
}

2. 建立主腳本

接著,建立 main.sh 來呼叫這些函數。

# main.sh
#!/bin/bash

# 取得腳本所在目錄,確保路徑正確
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# 載入模組
source "$SCRIPT_DIR/utils.sh"

# 主邏輯
main() {
    log_info "開始執行主程序"

    # 檢查必要命令
    check_command "curl" || exit 1

    # 使用模組中的函數
    local result=$(add_numbers 10 20)
    log_info "10 + 20 = $result"

    log_info "程序執行完畢"
}

# 執行主函數
main

執行 chmod +x main.sh 後,執行 ./main.sh 即可看到輸出結果。這種方式簡單直觀,適合小型專案。

方法二:使用 export -f 與子進程(進階)

當你需要在子進程(subshell)中呼叫函數時,source 無效,因為子進程擁有獨立的环境。此時可以透過 export -f 將函數匯出到環境變數中。

# export_module.sh
#!/bin/bash

# 定義函數
my_func() {
    echo "Hello from my_func!"
}

# 匯出函數到環境
export -f my_func

# 在子進程中呼叫
bash -c 'my_func'

注意:此方法依賴 BASH_ENV 或環境傳遞,在不同 Unix 系統間的可移植性較差,建議僅在特定環境下使用。

常見問題與解決方案

1. 路徑問題導致模組載入失敗

問題:當腳本被從不同目錄執行,或透過符號連結呼叫時,source ./utils.sh 可能找不到檔案。

解決方案:始終使用 BASH_SOURCE 來取得腳本所在的絕對路徑。

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "$SCRIPT_DIR/utils.sh"

這確保了無論從哪裡執行腳本,都能正確定位到模組檔案。

2. 變數作用域污染

問題:在模組中定義的全域變數可能會意外覆蓋主腳本中的變數,造成難以追蹤的錯誤。

解決方案

  • 在模組函數中使用 local 關鍵字宣告局部變數。
  • 避免在模組中直接修改全域狀態。如果必須共享設定,建議使用配置檔(如 .env)並明確載入。
# 錯誤示範
global_var="value" # 污染全域環境

# 正確示範
local global_var="value" # 僅在函數內有效

小結

模組化設計是 Shell 腳本從「可用」邁向「專業」的重要一步。透過將功能拆分為獨立檔案並使用 source 載入,我們能有效管理複雜邏輯,提升程式碼的維護性。雖然 Bash 並非為大型應用程式設計,但善用模組化技巧,仍能讓自動化腳本更加穩健與易於擴展。

建議在撰寫腳本前,先規劃好模組結構,將工具函數、業務邏輯與配置分離,這將為未來的擴充與協作打下良好基礎。