Shell 腳本中的模組化設計
在初學 Shell 腳本時,我們往往習慣將所有邏輯寫在單一檔案中,隨著功能增加,腳本會變得冗長且難以維護。這就像是在一個巨大的義大利麵(Spaghetti Code)中尋找特定邏輯一樣困難。對於經常處理系統管理、自動化部署或 CI/CD 腳本的工程師來說,模組化設計(Modular Design) 不僅是最佳實踐,更是確保腳本可讀性、可測試性與可重用性的關鍵。
本文將探討如何在 Bash 中實現模組化,並提供實際可執行的範例。
為什麼需要模組化?
模組化的核心在於「關注點分離」(Separation of Concerns)。透過將功能拆分為獨立的功能區塊或檔案,我們可以達到以下好處:
- 避免命名衝突:不同模組中的函數名稱不會互相干擾。
- 提升可讀性:主腳本只需關注流程控制,細節邏輯隱藏在模組中。
- 方便除錯與測試:單一函數可獨立測試,無需執行整個腳本。
- 重複使用:將通用工具函數提取為庫,供其他腳本呼叫。
方法一:使用 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 並非為大型應用程式設計,但善用模組化技巧,仍能讓自動化腳本更加穩健與易於擴展。
建議在撰寫腳本前,先規劃好模組結構,將工具函數、業務邏輯與配置分離,這將為未來的擴充與協作打下良好基礎。