Skip to content

健壮性:错误处理、退出码、trap 与模块引入

默认 shell 在命令失败时往往仍会往下执行,需要显式的错误处理策略。


一、退出码(Exit Code)

每个命令结束后都会返回一个 0–255 的退出码。

  • 0:成功。
  • 非 0:失败;具体含义依命令而定。
  • 特殊变量 $? 保存上一条命令的退出码。
shell
ls
echo "ls 退出码: $?"

ls non_existent_file.txt
echo "失败命令退出码: $?"

二、set -e(失败即退出)

shell
set -e   # 任一命令非 0 则终止脚本(errexit)

优点:避免错误无声扩散。
缺点:在 ifwhile&&|| 等上下文中的行为需查阅手册;有些命令「失败」是你想捕获的,需要临时关闭或 cmd || true

示例:

shell
set -e
echo "开始..."
ls
ls non_existent_file.txt   # 若 set -e 生效,脚本在此退出
echo "这行可能执行不到"

三、手动检查(最灵活)

推荐直接利用 if cmd 判断退出码,不必先跑再比 $?

shell
if mkdir my_new_dir; then
    echo "创建成功"
else
    echo "创建失败"
    exit 1
fi

若沿用旧稿风格显式看 $? 也可:

shell
mkdir my_new_dir
if [ $? -eq 0 ]; then
    echo "目录创建成功"
else
    echo "错误:无法创建目录"
    exit 1
fi

四、trap:捕获信号与 EXIT

在脚本结束(正常、异常、中断)时做清理:删临时文件、恢复 stty 等。

shell
cleanup() {
    echo -e "\n正在执行清理操作..."
    # rm -rf "$TEMP_DIR"
    echo "清理完毕。"
}

trap cleanup EXIT

echo "执行业务..."
sleep 2
ls non_existent_file.txt    # 可能失败

# 无论前面如何,EXIT 时通常会尝试运行 cleanup(若 shell 未当场退出)

常用信号:INT(Ctrl+C)、TERMEXIT。具体行为与是否启用 set -e 有关,复杂脚本建议分段测试。


五、终止脚本:exit

使用 exit [状态码] 结束整个脚本。函数里的 exit 也会结束整个脚本;若只想结束函数,请用 return

常见退出码约定

含义
0成功
1通用错误
2Shell 内置误用等
126命令不可执行(权限等)
127命令未找到
128exit 参数无效
128+n因信号 n 退出(如 130≈SIGINT)

示例:

shell
#!/bin/bash
FILENAME="example.txt"

if [ ! -f "$FILENAME" ]; then
    echo "错误:文件 $FILENAME 不存在。"
    exit 1
fi

echo "文件存在,继续..."
exit 0

注意exit 只能传 0–255;过大数值会被取模。


六、引入外部脚本:source 与 .

要把另一个脚本里的 变量、函数 加载到当前 Shell,使用:

shell
source ./config.sh
. ./config.sh          # 等价, POSIX 常用写法

config.sh 示例

shell
APP_NAME="MyApplication"
VERSION="1.0.0"

main.sh

shell
source ./config.sh
echo "Application Name: $APP_NAME"

与「直接执行子脚本」的区别

  • source / .:当前进程中执行,共享变量与函数不需要 chmod +x
  • ./other.shbash other.sh:多为子进程执行,默认反向污染当前脚本的变量(除非显式导出等)。

注意事项

  1. 路径:生产环境尽量用绝对路径或基于脚本目录推算路径。
  2. 权限:被 source 的文件不需要执行权限。
  3. exit 陷阱:被引入的脚本若执行 exit,会导致整个当前脚本退出;公共库文件应避免随意 exit,改为 return(若在函数中)或由调用方判断。

七、推荐阅读顺序

语法层面的符号、正则见 [Shell 语法进阶:特殊符号与正则](./06-Shell 语法进阶:特殊符号与正则表达式.md);运维向范例见 运维脚本示例 子目录。