文章目录

Shell 脚本错误处理:set -e 与 trap

发布于 2026-04-05 06:28:51 · 浏览 15 次 · 评论 0 条

Shell 脚本错误处理:set -e 与 trap

Shell 脚本运行过程中难免遇到各类错误:文件不存在、命令执行失败、权限不足……如果不做任何处理,脚本往往会带着错误状态继续执行,最终产生难以追溯的连锁问题。本文将介绍两个 Shell 错误处理的核心工具:set -etrap,帮助你写出更健壮的脚本。


为什么需要错误处理

当脚本中的某条命令执行失败时,默认情况下 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 -etrap 的用法,能让你的 Shell 脚本从"能运行"升级为"可靠运行"。前者提供了简洁的"快速失败"机制,后者则赋予了精细的捕获与响应能力。两者结合使用,即能应对大多数生产环境下的错误处理需求。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文