來看一個常見的場景
假設你正在開發一個數據備份腳本。這個腳本需要執行以下操作:
- 創建臨時工作目錄
- 將數據複製到臨時目錄
- 壓縮打包
- 清理臨時文件
#!/bin/bash
WORK_DIR="/tmp/backup_$(date +%Y%m%d)"
echo "開始備份..."
mkdir -p "$WORK_DIR"
echo "創建臨時目錄: $WORK_DIR"
echo "複製文件中..."
cp -r /path/to/data "$WORK_DIR/"
sleep 5 # 模擬耗時操作
echo "壓縮打包..."
tar -czf backup.tar.gz "$WORK_DIR"
sleep 3 # 模擬耗時操作
echo "清理臨時文件..."
rm -rf "$WORK_DIR"
echo "備份完成!"
如果我中斷了腳本怎麼辦!
當我們運行這個腳本時,如果在執行過程中按下 Ctrl+C 中斷操作,會發生什麼?
臨時目錄 $WORK_DIR 將被遺留在系統中,因為清理步驟沒有被執行。長期積累下來,這些未清理的臨時文件會佔用大量磁盤空間。
使用 trap 命令改善程序
這時,trap 命令就派上用場了。trap 可以捕獲特定的信號並執行相應的處理函數。SIGINT(通常由 Ctrl+C 觸發)就是最常見的信號之一。
首先,我們定義一箇中斷處理函數:
on_interrupt() {
echo -e "\n程序被中斷!"
echo "清理臨時文件..."
rm -rf "$WORK_DIR"
exit 1
}
然後,在腳本開頭使用 trap 設置信號處理:
trap on_interrupt SIGINT
完整的改進版腳本如下:
#!/bin/bash
WORK_DIR="/tmp/backup_$(date +%Y%m%d)"
# 定義中斷處理函數
on_interrupt() {
echo -e "\n程序被中斷!"
echo "清理臨時文件..."
rm -rf "$WORK_DIR"
exit 1
}
# 設置 trap
trap on_interrupt SIGINT
echo "開始備份..."
mkdir -p "$WORK_DIR"
echo "創建臨時目錄: $WORK_DIR"
echo "複製文件中..."
cp -r /path/to/data "$WORK_DIR/"
sleep 5 # 模擬耗時操作
echo "壓縮打包..."
tar -czf backup.tar.gz "$WORK_DIR"
sleep 3 # 模擬耗時操作
echo "清理臨時文件..."
rm -rf "$WORK_DIR"
echo "備份完成!"
trap 命令説明
trap 命令的基本語法是:
trap command signal
其中:
command可以是函數名或直接的命令signal是要捕獲的信號名稱,如 SIGINT、SIGTERM 等
常見的信號包括:
- SIGINT (2):用户按下 Ctrl+C
- SIGTERM (15):終止信號
- EXIT:腳本退出時
你還可以同時捕獲多個信號:
trap on_interrupt SIGINT SIGTERM
通過使用 trap 命令和 on_interrupt 函數,我們實現了:
- 優雅地處理程序中斷
- 確保臨時資源被正確清理
- 提供了友好的用户提示
這種模式不僅適用於備份腳本,還可以用在任何需要資源清理的腳本中,比如:
- 臨時文件處理
- 數據庫連接清理
- 鎖文件刪除
- 進程清理
擴展: trap 命令的高級應用
多信號處理
有時我們需要對不同的信號進行不同的處理。比如在一個數據處理腳本中:
#!/bin/bash
# 定義變量
DATA_FILE="data.txt"
TEMP_FILE="temp.txt"
LOG_FILE="process.log"
# 處理 Ctrl+C
on_interrupt() {
echo -e "\n收到 SIGINT,正在優雅關閉..."
cleanup
exit 1
}
# 處理 SIGTERM
on_terminate() {
echo -e "\n收到 SIGTERM,保存進度後退出..."
save_progress
cleanup
exit 1
}
# 處理正常退出
on_exit() {
echo "程序正常結束,執行清理..."
cleanup
}
# 清理函數
cleanup() {
rm -f "$TEMP_FILE"
echo "清理完成"
}
# 保存進度
save_progress() {
echo "保存當前進度到 $LOG_FILE"
echo "Progress saved at $(date)" >> "$LOG_FILE"
}
# 設置多重信號處理
trap on_interrupt SIGINT
trap on_terminate SIGTERM
trap on_exit EXIT
# 主程序
echo "開始處理數據..."
while true; do
echo "處理中..."
sleep 1
done
臨時禁用和恢復信號處理
有時我們需要臨時禁用信號處理,比如在執行關鍵操作時:
#!/bin/bash
critical_operation() {
# 臨時禁用 Ctrl+C
trap '' SIGINT
echo "執行關鍵操作,這段時間按 Ctrl+C 無效..."
sleep 5
# 恢復信號處理
trap on_interrupt SIGINT
echo "關鍵操作完成,恢復正常信號處理"
}
on_interrupt() {
echo -e "\n操作被中斷!"
exit 1
}
trap on_interrupt SIGINT
echo "開始執行..."
critical_operation
echo "繼續其他操作..."
DEBUG 信號與調試處理
DEBUG 並不是中斷信號,而是 Bash 的一個特殊 trap 事件。它在執行每個命令之前觸發,主要用於調試目的。讓我們看一個更實用的例子:
#!/bin/bash
# 用於控制是否在錯誤處理函數中觸發 DEBUG trap
IN_ERROR_HANDLER=0
# 定義調試處理函數
on_debug() {
# 如果在錯誤處理函數中,跳過調試輸出
if ((IN_ERROR_HANDLER)); then
return
fi
# $1 是行號,$BASH_COMMAND 是即將執行的命令
echo "[DEBUG] 行 $1: 準備執行 -> $BASH_COMMAND"
}
# 錯誤處理函數
on_error() {
local err=$? # 立即保存錯誤碼
local line=$1
local cmd=$2
# 設置標誌,防止在錯誤處理中觸發 DEBUG trap
IN_ERROR_HANDLER=1
echo "[ERROR] 行 $line 執行失敗"
echo "命令: $cmd"
echo "錯誤碼: $err"
# 重置標誌
IN_ERROR_HANDLER=0
}
# 啓用調試跟蹤
enable_debug() {
# 啓用 ERR trap
set -E
# -T 選項可以顯示函數調用跟蹤
set -T
# 設置 DEBUG trap,傳入行號參數
trap 'on_debug ${LINENO}' DEBUG
trap 'on_error ${LINENO} "$BASH_COMMAND"' ERR
}
# 關閉調試跟蹤
disable_debug() {
trap - DEBUG
trap - ERR
set +E
set +T
}
# 通過環境變量控制是否開啓調試
if [[ "${ENABLE_DEBUG}" == "true" ]]; then
enable_debug
fi
# 測試函數
test_function() {
echo "執行測試函數"
local result=$((2 + 2))
echo "計算結果: $result"
# 故意製造一個錯誤
ls /nonexistent_directory
}
# 主程序
echo "開始執行..."
test_function
echo "嘗試訪問不存在的文件..."
cat nonexistent_file.txt
使用方式:
# 普通執行
./script.sh
# 開啓調試模式執行
ENABLE_DEBUG=true ./script.sh
普通模式輸出:
開始執行...
執行測試函數
計算結果: 4
ls: cannot access '/nonexistent_directory': No such file or directory
嘗試訪問不存在的文件...
cat: nonexistent_file.txt: No such file or directory
DEBUG 模式輸出:
[DEBUG] 行 41: 準備執行 -> trap 'on_error ${LINENO} "$BASH_COMMAND"' ERR
[DEBUG] 行 67: 準備執行 -> echo "開始執行..."
開始執行...
[DEBUG] 行 68: 準備執行 -> test_function
[DEBUG] 行 58: 準備執行 -> test_function
[DEBUG] 行 59: 準備執行 -> echo "執行測試函數"
執行測試函數
[DEBUG] 行 60: 準備執行 -> local result=$((2 + 2))
[DEBUG] 行 61: 準備執行 -> echo "計算結果: $result"
計算結果: 4
[DEBUG] 行 63: 準備執行 -> ls /nonexistent_directory
ls: cannot access '/nonexistent_directory': No such file or directory
[DEBUG] 行 63: 準備執行 -> ls /nonexistent_directory
[DEBUG] 行 17: 準備執行 -> ls /nonexistent_directory
[DEBUG] 行 18: 準備執行 -> local err=$?
[DEBUG] 行 19: 準備執行 -> local line=$1
[DEBUG] 行 20: 準備執行 -> local cmd=$2
[DEBUG] 行 23: 準備執行 -> IN_ERROR_HANDLER=1
[ERROR] 行 63 執行失敗
命令: ls /nonexistent_directory
錯誤碼: 2
[DEBUG] 行 68: 準備執行 -> ls /nonexistent_directory
[DEBUG] 行 17: 準備執行 -> ls /nonexistent_directory
[DEBUG] 行 18: 準備執行 -> local err=$?
[DEBUG] 行 19: 準備執行 -> local line=$1
[DEBUG] 行 20: 準備執行 -> local cmd=$2
[DEBUG] 行 23: 準備執行 -> IN_ERROR_HANDLER=1
[ERROR] 行 68 執行失敗
命令: ls /nonexistent_directory
錯誤碼: 2
[DEBUG] 行 69: 準備執行 -> echo "嘗試訪問不存在的文件..."
嘗試訪問不存在的文件...
[DEBUG] 行 70: 準備執行 -> cat nonexistent_file.txt
cat: nonexistent_file.txt: No such file or directory
[DEBUG] 行 70: 準備執行 -> cat nonexistent_file.txt
[DEBUG] 行 17: 準備執行 -> cat nonexistent_file.txt
[DEBUG] 行 18: 準備執行 -> local err=$?
[DEBUG] 行 19: 準備執行 -> local line=$1
[DEBUG] 行 20: 準備執行 -> local cmd=$2
[DEBUG] 行 23: 準備執行 -> IN_ERROR_HANDLER=1
[ERROR] 行 70 執行失敗
命令: cat nonexistent_file.txt
錯誤碼: 1
文件鎖機制 trap vs flock
讓我們比較 trap 和 flock 的鎖機制:
使用 trap 的文件鎖
#!/bin/bash
LOCK_FILE="/tmp/script.lock"
PID_FILE="/tmp/script.pid"
cleanup() {
rm -f "$LOCK_FILE" "$PID_FILE"
echo "清理鎖文件和PID文件"
}
get_lock() {
if [ -e "$LOCK_FILE" ]; then
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "另一個實例(PID: $pid)正在運行"
exit 1
fi
# 如果進程不存在,清理舊的鎖
cleanup
fi
echo $$ > "$PID_FILE"
touch "$LOCK_FILE"
trap cleanup EXIT
}
使用 flock 的實現:
#!/bin/bash
LOCK_FILE="/tmp/script.lock"
(
# 獲取文件鎖,等待最多5秒
flock -w 5 200 || { echo "無法獲取鎖,另一個實例正在運行"; exit 1; }
echo "獲得鎖,開始執行..."
sleep 10
echo "執行完成"
) 200>"$LOCK_FILE"
比較分析
-
可靠性
- flock 更可靠,它使用內核級文件鎖
- trap 方式可能在極端情況下(如系統崩潰)留下孤立的鎖文件
-
使用場景
- flock 適合要求嚴格的生產環境
- trap 方式適合簡單的腳本和開發環境
-
推薦選擇
-
推薦使用 flock,因為它:
- 自動處理進程終止
- 支持超時設置
- 提供阻塞和非阻塞模式
- 可靠性更高
-
事務的實現
#!/bin/bash
# 狀態變量
TRANSACTION_ACTIVE=false
# 動態改變信號處理
update_signal_handler() {
if $TRANSACTION_ACTIVE; then
# 事務進行中,設置中斷處理為提示並結束
trap 'echo "事務進行中,已被強行中斷..."; cleanup; exit 1' SIGINT
else
# 非事務狀態,可以安全退出
trap 'echo "正常退出..."; exit 0' SIGINT
fi
}
# 清理函數
cleanup() {
echo "執行清理操作..."
# 這裏添加實際的清理代碼
}
# 模擬事務
start_transaction() {
TRANSACTION_ACTIVE=true
update_signal_handler
echo "事務開始"
# 模擬事務操作
echo "執行事務步驟 1/3"
sleep 2
echo "執行事務步驟 2/3"
sleep 2
echo "執行事務步驟 3/3"
sleep 2
TRANSACTION_ACTIVE=false
update_signal_handler
echo "事務完成"
}
# 設置初始信號處理
update_signal_handler
# 主程序執行流程
echo "開始執行..."
start_transaction
echo "繼續其他操作..."
執行流程説明:
-
腳本啓動
TRANSACTION_ACTIVE初始值為false- 首次調用
update_signal_handler,設置正常的中斷處理
-
執行
start_transaction- 設置
TRANSACTION_ACTIVE為true - 更新信號處理為事務保護模式
- 執行事務操作
- 完成後,設置
TRANSACTION_ACTIVE為false - 恢復正常的信號處理
- 設置
-
信號處理行為
- 事務進行中收到 SIGINT:顯示中斷消息,執行清理,然後退出
- 非事務狀態收到 SIGINT:直接安全退出
通過這些高級用法,我們可以構建更健壯、更可靠的 shell 腳本。無論是處理意外中斷、實現鎖機制,還是進行調試,trap 都是一個強大的工具。