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 directorycp 失败脚本直接退出后面的 echo 不会跑。没有-e的话echo 照常执行你以为一切正常。但 -e 有例外命令在if、while条件里或者在/||链中、或者命令以!开头时失败不触发退出。这是故意的因为你本来就是在判断失败。set -u空变量立刻报错set-ufnamerm$fname没有-u时变成rm bash 报缺少操作数或者更糟——某些写法下误删当前目录内容。有-u直接rm: missing operand可选变量用${var:-default}既满足-u又给默认值这是脚本里最常见的写法。set -x每步打出来set-xnameBenechohi$name预期输出 nameBen echo hi Ben hi Ben之后就是命令真正执行时的样子。看变量到底传了什么、文件名是不是被展开靠它。调试完set x关掉。pipefailset-opipefailmake|teebuild.log没有pipefail时管道退出码是最后一个命令tee的。make 编译失败tee 成功脚本以为一切正常。加了pipefailmake 失败整条管道就失败。这条单独记不住就和-euo写在一起。二、手动调试bash -x 和 PS4不改脚本文件直接bash-x./deploy.sh每步 trace 打到标准错误不影响正常输出。想在 trace 里看到行号和文件名改PS4exportPS4 [${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 / DebiansudoaptinstallshellcheckCentOS / RockysudodnfinstallShellCheck也有在线版 shellcheck.net把脚本贴上去就能出结果不用装。跑一下shellcheckdeploy.sh常见检查项编号含义怎么改SC2086变量未加引号会分词和展开通配改成$varSC2034变量赋值后从未使用删掉或用掉SC2164cd 失败后继续执行cd ... || exitSC2006用了反引号 改成$(...)SC2015A B || C这种链不可靠改成 if/elseSC2166[ -n $var -a ... ]改成两个[ ]用SC2086$在某些上下文要写成$保留引号示例一段有问题的脚本#!/bin/bashdir/var/logcd$dirfilesls*.logecho$filesshellcheck输出节选In deploy.sh line 3: cd $dir ^-- SC2164: Use cd ... || exit or cd ... || return in case cd fails. In deploy.sh line 4: filesls *.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 容易被坑的地方函数里local var$(cmd)局部赋值的退出码是local内建自己的不是 cmd 的-e不会触发。拆开写var$(cmd)localvar或者local var; var$(cmd)。cmd | grep somethinggrep 没匹配返回 1带pipefail会直接退出。只想知道有没有包到if里ifecho$output|grep-qsomething;thenecho命中fi管道里想忽略某个命令失败用|| truerm-f/tmp/*.tmp||trueset -e在子 shell 和函数里的继承不完整配合-Eerrtrace和-Tfunctrace才能让trap ERR跟着进函数。别把-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和赋值别写一行临时插一行调试最常用的是把变量打到标准错误重定向不混进正常输出echodbg: host$host, port$port2调试完删掉这行就行比在文件里到处set -x/set x干净。-x和-v的区别顺带说一下-v是把脚本原文打印一遍你能看到每行长什么样-x是把变量展开后的实际命令打印一遍。排查变量到底变成什么了用-x排查这行为什么没执行用-v。
返回列表