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。

相关推荐
Carol063016 分钟前
上肢及肩肘关节
java·服务器·数据库
帷幕落秋19 分钟前
SSH远程管理服务
运维·ssh
一世繁华行36 分钟前
CAPL浏览器(翻译自官方文档)
运维·服务器·数据库
万联WANFLOW44 分钟前
让分散的设备像在同一张局域网里:企业组网技术解析
运维·网络·网络安全·智能路由器
风翼靓崽1 小时前
ubuntu22.04 rsyslog分离ssh单独日志文件
运维·ssh
q27551300421 小时前
AS8132 DP转HDMI 4K60(420)被动式 国产性价比AS方案
linux·算法·硬件架构·dash
刚入门的大一新生1 小时前
Linux-进程间通信2
linux·运维·服务器
电科_银尘2 小时前
【深度学习项目实战·02】Linux服务器环境搭建:从SSH登录到稳定运行项目
linux·服务器·深度学习
Yana.nice2 小时前
清除密码历史记录,解决pam安全策略密码无法复用问题
运维
海宇服务2 小时前
零信任架构实战:基于海宇风控经营异常预警构建自动化央企物资采购供应商资信排查网关
运维·人工智能·架构·自动化