Shell脚本调试与最佳实践:set -eux、shellcheck实战

Shell 脚本出 bug,往往不是逻辑错,而是一个变量为空、一条命令静默失败,脚本带着错误结果继续往下跑。set -eux 和 shellcheck 是两味药:前者让脚本一出错就停、把每步执行打出来;后者静态扫一遍常见写法错误。这一篇把它们讲清楚,再配一个能直接抄进脚本头部的"保险套装"。

一、set 的几个开关

开关 作用
-e 命令失败(非零退出)立即退出
-u 引用未定义变量时报错退出
-x 执行前打印命令展开结果(调试用)
-o pipefail 管道里任一命令失败,整条管道算失败
-E ERR 陷阱被子 shell / 函数继承
-n 只读不执行,做语法检查

生产脚本开头一行组合用:

bash 复制代码
set -euo pipefail

这一行的含义:命令失败就停、空变量立刻报、管道任何一环失败都算失败。

set -e:出错就停

bash 复制代码
set -e
cp /etc/does-not-exist /tmp/x
echo "这行不会执行"

预期输出:

text 复制代码
cp: cannot stat '/etc/does-not-exist': No such file or directory

cp 失败,脚本直接退出,后面的 echo 不会跑。没有 -e 的话,echo 照常执行,你以为一切正常。

但 -e 有例外 :命令在 if、while 条件里,或者在 && / || 链中、或者命令以 ! 开头时,失败不触发退出。这是故意的,因为你本来就是在判断失败。

set -u:空变量立刻报错

bash 复制代码
set -u
fname=""
rm "$fname"

没有 -u 时变成 rm "",bash 报"缺少操作数",或者更糟------某些写法下误删当前目录内容。有 -u 直接:

text 复制代码
rm: missing operand

可选变量用 ${var:-default},既满足 -u 又给默认值,这是脚本里最常见的写法。

set -x:每步打出来

bash 复制代码
set -x
name=Ben
echo "hi $name"

预期输出:

text 复制代码
+ name=Ben
+ echo 'hi Ben'
hi Ben

+ 之后就是命令真正执行时的样子。看变量到底传了什么、文件名是不是被展开,靠它。调试完 set +x 关掉。

pipefail

bash 复制代码
set -o pipefail
make | tee build.log

没有 pipefail 时,管道退出码是最后一个命令(tee)的。make 编译失败,tee 成功,脚本以为一切正常。加了 pipefail,make 失败整条管道就失败。这条单独记不住,就和 -euo 写在一起。

二、手动调试:bash -x 和 PS4

不改脚本文件,直接:

bash 复制代码
bash -x ./deploy.sh

每步 trace 打到标准错误,不影响正常输出。想在 trace 里看到行号和文件名,改 PS4:

bash 复制代码
export PS4='+ [${BASH_SOURCE}:${LINENO}] '
bash -x ./deploy.sh

输出变成:

text 复制代码
+ [deploy.sh:12] cp /etc/nginx.conf /tmp/
+ [deploy.sh:13] echo 完成

定位快很多。只想检查语法不跑:

bash 复制代码
bash -n ./deploy.sh

有错报行号,没错静默退出。

三、shellcheck:静态扫错

安装

Ubuntu / Debian:

bash 复制代码
sudo apt install shellcheck

CentOS / Rocky:

bash 复制代码
sudo dnf install ShellCheck

也有在线版 shellcheck.net,把脚本贴上去就能出结果,不用装。

跑一下

bash 复制代码
shellcheck deploy.sh

常见检查项:

编号 含义 怎么改
SC2086 变量未加引号,会分词和展开通配 改成 "$var"
SC2034 变量赋值后从未使用 删掉或用掉
SC2164 cd 失败后继续执行 `cd ...
SC2006 用了反引号 改成 $(...)
SC2015 `A && B
SC2166 [ -n "$var" -a ... ] 改成两个 [ ] 用 &&
SC2086 $@ 在某些上下文要写成 "$@" 保留引号

示例:一段有问题的脚本

bash 复制代码
#!/bin/bash
dir=/var/log
cd $dir
files=`ls *.log`
echo $files

shellcheck 输出(节选):

text 复制代码
In deploy.sh line 3:
cd $dir
   ^-- SC2164: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

In deploy.sh line 4:
files=`ls *.log`
     ^-- SC2006: Use $(...) instead of `...`.

In deploy.sh line 5:
echo $files
     ^-- SC2086: Double quote to prevent globbing and word splitting.

改完:

bash 复制代码
#!/bin/bash
set -euo pipefail
dir=/var/log
cd "$dir" || exit 1
files=$(ls *.log)
echo "$files"

四、⚠️ set -e 容易被坑的地方

  1. 函数里 local var=$(cmd):局部赋值的退出码是 local 内建自己的,不是 cmd 的,-e 不会触发。拆开写:

    bash 复制代码
    var=$(cmd)
    local var

    或者 local var; var=$(cmd)。

  2. cmd | grep something,grep 没匹配返回 1,带 pipefail 会直接退出。只想知道有没有,包到 if 里:

    bash 复制代码
    if echo "$output" | grep -q something; then
      echo "命中"
    fi
  3. 管道里想忽略某个命令失败,用 || true:

    bash 复制代码
    rm -f /tmp/*.tmp || true
  4. set -e 在子 shell 和函数里的继承不完整,配合 -E(errtrace)和 -T(functrace)才能让 trap ERR 跟着进函数。

  5. 别把 -e 当安全网,关键步骤后面显式判断退出码才是稳的。

五、知识扩展:shellcheck 在检查什么

shellcheck 不执行脚本,它把 bash 解析成语法树,再套用一组模式规则。每条 SC 编号就是一条规则,规则背后往往是一个真实踩坑案例:SC2086 对应"文件名带空格导致 ls 出来的列表被拆开",SC2164 对应"cd 到不存在的目录,后续操作全在错误路径上"。所以它报出来的不是风格建议,是 bug 候选------能改就改,拿不准就去官网查这条编号的解释。

set -e 历史上一直有争议。POSIX 规定它在很多情况下不生效(if 条件、&& 链、函数调用),不同 shell 实现还不一样。bash 4.4 加了 set -o errtrace(即 -E)和 set -o functrace 改善子 shell 行为。写跨发行版脚本时,别完全依赖 -e,关键步骤后面显式 || { echo 失败; exit 1; } 才是稳的。

调试脚本有个朴素原则:先 bash -n 过语法,再 shellcheck 过写法,再 bash -x 跑一遍看变量。三步下来,九成问题在这一关就暴露了,不用等到服务器上才发现。

六、常见调试场景速查

现象 怎么办
脚本一跑就报"未定义变量" 注释掉 set -u,或者给变量加 ${var:-}
不知道变量传了什么 临时 set -x,或在关键行后 echo "dbg: $var" >&2
语法错误但不知道哪行 bash -n script.sh,报错会给行号
某些命令失败脚本还继续跑 检查是不是在 if 条件或 && 链里,那是正常的
函数里 local var=$(cmd) 失败没退出 拆开 local 和赋值,别写一行

临时插一行调试,最常用的是把变量打到标准错误(重定向不混进正常输出):

bash 复制代码
echo "dbg: host=$host, port=$port" >&2

调试完删掉这行就行,比在文件里到处 set -x / set +x 干净。

-x 和 -v 的区别顺带说一下:-v 是把脚本原文打印一遍,你能看到每行长什么样;-x 是把变量展开后的实际命令打印一遍。排查"变量到底变成什么了"用 -x;排查"这行为什么没执行"用 -v。

相关推荐
海宇AI1 小时前
零信任架构实战:基于海宇活体识别V步骤1构建自动化直播开播鉴权网关
运维·人工智能·架构·自动化
ZeroNews内网穿透1 小时前
企业内网穿透安全避坑:路由白名单,解决整站映射带来的接口泄露风险
运维·内网穿透·api安全·zeronews·企业网络安全·运维实践
qetfw1 小时前
Debian iSCSI Target 与 open-iscsi:LVM 后端、ACL 与客户端验证
linux·服务器·debian·php
苦瓜不会码代码1 小时前
负载均衡会话保持-扩容后登录掉线踩坑
运维·负载均衡
pt10432 小时前
CML网络仿真入门-1:Cisco Modeling Labs概述
运维·网络协议
谷哥的小弟2 小时前
Linux服务器网络服务与端口
linux·运维·服务器·端口
Mortalbreeze2 小时前
MySQL 基础篇(四):表的约束
linux·服务器·数据库·mysql
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-71-存过执行过程中重建存过影响验证
linux·运维·数据库·sql·学习