Shell 脚本错误处理:set -e 与 trap
Shell 脚本运行过程中难免遇到各类错误:文件不存在、命令执行失败、权限不足……如果不做任何处理,脚本往往会带着错误状态继续执行,最终产生难以追溯的连锁问题。本文将介绍两个 Shell 错误处理的核心工具:set -e 与 trap,帮助你写出更健壮的脚本。
为什么需要错误处理
当脚本中的某条命令执行失败时,默认情况下 Shell 会继续执行后续命令。这种"沉默失败"的特性看似无害,实则暗藏风险。试想一个场景:你的脚本先切换到某个目录,然后执行删除操作。如果切换目录失败(目录不存在),脚本仍会继续执行删除命令——此时删除的可能是错误位置的文件。
错误处理的核心目标是:让脚本在遇到问题时立即停止、记录日志、执行清理,或按照预定策略应对。set -e 提供了"遇错即停"的简单方案,trap 则提供了更灵活的捕获与响应机制。
set -e:遇错即停
基本概念
set -e 是 Shell 的一个内置选项,全称是 "errexit"(error exit)。启用后,当脚本中任何命令的返回状态码非零(即执行失败)时,Shell 会立即退出整个脚本,不再执行后续命令。
启用方式
有两种方法启用 set -e:
方法一:在脚本开头全局启用
#!/bin/bash
set -e
cd /some/directory
mkdir newfolder
# 如果上面任何命令失败,脚本立即退出
方法二:在特定代码块中局部启用
#!/bin/bash
# 临时启用 errexit
set -e
可能失败的命令
set +e # 恢复默认行为(遇错不退出)
# 后续代码继续执行
set +e 用于关闭 set -e 效果,恢复到默认的"遇错继续"模式。
注意事项
使用 set -e 时需要特别注意以下几点:
1. 条件判断中的命令
在 if 语句中,即使命令返回非零状态码,脚本也不会退出。这是因为 if 结构本身就会检查命令状态,继续执行是预期行为:
#!/bin/bash
set -e
if grep -q "pattern" file.txt; then
echo "Found pattern"
else
echo "Pattern not found"
fi
# grep 返回非零时不会触发 set -e,因为处于 if 上下文中
2. 管道命令
管道连接的命令中,只要最后一个命令成功,整个管道就视为成功。这意味着管道中任意位置的命令失败可能被忽略:
#!/bin/bash
set -e
cat file.txt | grep "keyword" | sort
# 如果 file.txt 不存在,cat 会失败
# 但由于 grep 和 sort 可能成功,管道整体返回 0,set -e 不会触发
解决方法是使用 set -o pipefail 选项:
#!/bin/bash
set -e
set -o pipefail
cat file.txt | grep "keyword" | sort
# 现在管道中任何命令失败都会导致脚本退出
3. 变量赋值的命令
直接赋值的命令失败不会触发 set -e:
#!/bin/bash
set -e
result=$(command_that_fails) # 如果命令失败,result 为空,但脚本继续
echo "Script continues"
```
---
## trap:捕获信号与错误
### 基本概念
`trap` 是 Shell 提供的另一个强大工具,用于捕获特定信号或错误条件,并执行预设的命令。它的作用类似于其他编程语言中的"异常捕获"机制,不仅能处理命令失败,还能响应系统信号(如 `Ctrl+C` 发送的 `SIGINT`)。
### 语法格式
```bash
trap 'command1; command2' SIGNAL [SIGNAL ...]
trap 'command' ERR EXIT RETURN
```
其中:
- 单引号包裹的是要执行的命令
- `SIGNAL` 可以是信号名称(如 `INT`、`TERM`)或编号
- `ERR`、`EXIT`、`RETURN` 是特殊的事件标识
### 捕获命令执行错误(ERR)
```bash
#!/bin/bash
trap 'echo "Error on line $LINENO: command failed" >&2; exit 1' ERR
set -e
mkdir /important/directory
touch /important/directory/file.txt
当任何命令执行失败时,trap 会打印错误发生的行号,然后退出脚本。
捕获退出信号(EXIT)
EXIT 会在脚本正常或异常退出时触发,常用于清理工作:
#!/bin/bash
# 创建临时文件
TEMPFILE=$(mktemp)
# 注册退出清理
trap 'rm -f "$TEMPFILE"; echo "Cleanup done"' EXIT
# 脚本主体逻辑
echo "Processing..." > "$TEMPFILE"
sleep 10
```
无论脚本是因为成功完成、执行出错,还是被用户强制中断(只要不是 `kill -9` 这种无法捕获的信号),`trap` 都会执行清理命令。
### 捕获中断信号(INT)
当用户按下 `Ctrl+C` 时,会发送 `SIGINT` 信号。捕获此信号可以执行优雅的停止逻辑:
```bash
#!/bin/bash
cleanup() {
echo "Received interrupt. Cleaning up..."
# 停止后台进程
kill $BACKGROUND_PID 2>/dev/null
exit 1
}
trap cleanup INT
# 启动后台任务
sleep 100 &
BACKGROUND_PID=$!
# 等待后台任务
wait $BACKGROUND_PID
捕获多个信号
一个 trap 可以同时响应多个信号:
#!/bin/bash
error_handler() {
echo "Error occurred at line $LINENO"
cleanup
}
interrupt_handler() {
echo "Interrupted by user"
cleanup
exit 1
}
trap error_handler ERR
trap interrupt_handler INT TERM
# 脚本主体
```
---
## 实际应用场景
### 场景一:数据库备份脚本
```bash
#!/bin/bash
set -e
set -o pipefail
LOGFILE="/var/log/backup.log"
BACKUP_DIR="/backup"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
}
cleanup() {
log "Cleanup temporary files..."
rm -f /tmp/backup_*.sql
}
trap cleanup EXIT
log "Starting backup..."
# 导出数据库
mysqldump -u root -p"$DB_PASS" mydb > "/tmp/backup_$(date +%Y%m%d).sql"
log "Database exported successfully"
# 压缩备份
gzip "/tmp/backup_$(date +%Y%m%d).sql"
mv "/tmp/backup_$(date +%Y%m%d).sql.gz" "$BACKUP_DIR/"
log "Backup completed and moved to $BACKUP_DIR"
场景二:部署脚本的确认机制
#!/bin/bash
# 关闭默认的遇错退出,改为手动控制
set +e
# 尝试执行部署
cd /opt/myapp
git pull origin main
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "Git pull failed. Checking for local changes..."
git status
echo "Do you want to continue anyway? (y/n)"
read -r answer
if [ "$answer" != "y" ]; then
echo "Deployment aborted."
exit 1
fi
fi
# 重新启用错误检查
set -e
# 重启服务
systemctl restart myapp
```
---
## 最佳实践总结
**1. 明确你的错误处理策略**
在编写脚本前,先思考几个问题:哪些命令失败是致命的?哪些可以容忍?是否需要清理临时文件?是否需要记录详细日志?明确这些后再选择合适的处理方式。
**2. 推荐的基础配置**
对于大多数脚本,推荐使用以下组合:
```bash
#!/bin/bash
set -euo pipefail
```
其中:
- `-e`:遇错即停
- `-u`:使用未定义的变量时报错(防止拼写错误导致的问题)
- `-o pipefail`:管道中任何命令失败都触发 `-e`
**3. 正确区分 ERR 与 EXIT**
`ERR` 陷阱只在命令失败时触发,而 `EXIT` 陷阱在脚本退出时必定触发。如果需要区分错误处理和清理工作,两者都要用:
```bash
trap 'handle_error' ERR
trap 'cleanup' EXIT
```
**4. 传递性陷阱**
在函数或 sourced 脚本中,`RETURN` 陷阱可以捕获"返回"事件,常用于实现局部cleanup逻辑:
```bash
function with_temp_file() {
local tmp=$(mktemp)
trap 'rm -f "$tmp"' RETURN
# 函数逻辑
}
掌握 set -e 与 trap 的用法,能让你的 Shell 脚本从"能运行"升级为"可靠运行"。前者提供了简洁的"快速失败"机制,后者则赋予了精细的捕获与响应能力。两者结合使用,即能应对大多数生产环境下的错误处理需求。

暂无评论,快来抢沙发吧!