拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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

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

一、set 的几个开关

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

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

set-euopipefail

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

set -e:出错就停

set-ecp/etc/does-not-exist /tmp/xecho"这行不会执行"

预期输出:

cp: cannot stat '/etc/does-not-exist': No such file or directory

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

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

set -u:空变量立刻报错

set-ufname=""rm"$fname"

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

rm: missing operand

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

set -x:每步打出来

set-xname=Benecho"hi$name"

预期输出:

+ name=Ben + echo 'hi Ben' hi Ben

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

pipefail

set-opipefailmake|teebuild.log

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

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

不改脚本文件,直接:

bash-x./deploy.sh

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

exportPS4='+ [${BASH_SOURCE}:${LINENO}] 'bash-x./deploy.sh

输出变成:

+ [deploy.sh:12] cp /etc/nginx.conf /tmp/ + [deploy.sh:13] echo 完成

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

bash-n./deploy.sh

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

三、shellcheck:静态扫错

安装

Ubuntu / Debian:

sudoaptinstallshellcheck

CentOS / Rocky:

sudodnfinstallShellCheck

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

跑一下

shellcheckdeploy.sh

常见检查项:

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

示例:一段有问题的脚本

#!/bin/bashdir=/var/logcd$dirfiles=`ls*.log`echo$files

shellcheck输出(节选):

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.

改完:

#!/bin/bashset-euopipefaildir=/var/logcd"$dir"||exit1files=$(ls*.log)echo"$files"

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

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

    var=$(cmd)localvar

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

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

    ifecho"$output"|grep-qsomething;thenecho"命中"fi
  3. 管道里想忽略某个命令失败,用|| true:

    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和赋值,别写一行

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

echo"dbg: host=$host, port=$port">&2

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

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

返回列表