一句话总结
脚本从"能跑"到"靠谱",差的就是这一篇:用函数把逻辑拆成可复用的块,用位置参数接收外部输入,用 set -euo pipefail 让脚本一出错就停,用 trap 保证退出时清理现场.
这些是把玩具脚本变成生产脚本的工程化手段.
函数:把逻辑打包复用
当一段逻辑在脚本里要用好几次,或者脚本长到需要拆分时,就该用函数.Shell 函数定义很简洁:
|
调用函数不用括号
定义时写 greet(),但调用时不写括号:直接 greet "alice",参数像命令行参数一样空格分隔.写成 greet("alice") 是其他语言的习惯,在 Shell 里是错的.
函数的参数与返回
函数内部用 $1 $2 ... 取参数--和脚本接收命令行参数用的是同一套位置参数机制,只是作用域变成了函数内部.
|
return 返回的是退出码,不是数据
Shell 函数的 return N 只能返回 0–255 的退出码(表示成功/失败),不能返回字符串或大数字.要"返回数据",标准做法是函数内 echo 结果,调用方用 $(...) 捕获--就像上面 result=$(add 3 5).把 return 当成"成功与否的信号",把 echo 当成"数据出口".
变量默认全局,务必用 local
Shell 函数里的变量默认是全局的--函数内改了一个同名变量,会污染函数外.养成习惯:函数内的临时变量一律加 local 声明,避免难以排查的"变量被偷偷改掉"的 bug.
脚本参数:与外界的接口
脚本通过位置参数接收命令行传入的值,这是脚本最常见的输入方式.一套特殊变量专门用于此:
| 变量 | 含义 |
|---|---|
$0 |
脚本自身的名字 |
$1 $2 ... $9 |
第 1,2...个参数;两位数要写 ${10} |
$# |
参数个数 |
$@ |
所有参数(各自独立,加引号后每个参数完整保留) |
$* |
所有参数(合并成一个字符串) |
$? |
上一条命令的退出码 |
$$ |
当前脚本进程的 PID |
|
都加双引号时:"$@" 展开成多个独立的,各自带引号的参数("foo" "bar" "baz");"$*" 展开成一个字符串("foo bar baz").需要原样转发参数给另一个命令时,永远用 "$@"--它能正确处理含空格的参数.
参数校验与默认值
|
${变量:-默认值} 是极常用的"取值,没有就用默认"语法,让脚本更健壮.
退出码:脚本的成败信号
脚本本身也要向调用它的人(或 CI,cron)报告成败,靠的是 exit N:
exit 0--成功(也是默认,脚本正常跑完退出码就是最后一条命令的)exit 1及其他非零值--失败,不同数字可表示不同错误类型
|
退出码是脚本交班时的"工作报告":0 是"一切顺利",非 0 是"出事了,而且这个数字告诉上层是哪类事".
上层程序(如 && 链,CI 流水线)正是靠读这个数字决定要不要继续往下走.
严格模式:set -euo pipefail
Shell 默认非常宽容--命令失败了还往下跑,用了未定义变量当空串处理,管道中间出错被忽略.这种"装作没事"的默认行为是脚本事故的温床.生产脚本几乎都会在开头加这一行开启严格模式:
|
| 选项 | 作用 |
|---|---|
set -e |
任何命令失败(非零退出)立即终止脚本,不再硬着头皮往下跑 |
set -u |
使用未定义变量时报错退出,防止拼错变量名导致的诡异行为 |
set -o pipefail |
管道中任何一环失败,整条管道就算失败(默认只看最后一个命令) |
为什么 pipefail 重要
默认情况下 cat 不存在的文件 | grep foo 的退出码取决于 grep(最后一个命令),cat 的失败被吞掉了.加上 pipefail 后,只要管道里 cat 失败,整条管道就判失败,配合 set -e 脚本会立即停下--避免基于残缺数据继续执行.
set -e 的例外:允许某条命令失败
开了 set -e 后,如果明知某条命令可能失败且不想让脚本退出,用 || true 兜底:grep "可能不存在" file || true.或者把它放进 if 里判断--if 条件中的命令失败不会触发 set -e.
trap:退出时的清理钩子
脚本经常会创建临时文件,占用锁,起后台进程.如果脚本中途出错退出,这些残留物不会自动清理.trap 让你注册一个"无论怎么退出都会执行"的清理函数:
|
trap 函数 信号 可以挂到不同事件上:
| 信号/事件 | 触发时机 |
|---|---|
EXIT |
脚本以任何方式退出时--最常用,清理收尾 |
INT |
用户按 Ctrl-C 时(回顾第 14 篇信号) |
TERM |
收到 SIGTERM 终止信号时 |
ERR |
有命令出错时(配合 set -e 做错误日志) |
trap ... EXIT 就像出门前设的"自动关灯锁门"程序--无论是正常下班,临时被叫走(Ctrl-C),还是中途绊倒(出错退出),离开时灯总会关,门总会锁.
临时文件,锁,后台进程的清理交给它,就不怕脚本异常退出后留一地垃圾.
组合:一个健壮脚本的骨架
把这一篇的要点拼起来,就是一个生产级脚本的标准开头:
|
把这个骨架存成模板
严格模式 + trap 清理 + log/die 辅助函数 + 参数校验,几乎是每个正经脚本都需要的.把它存成一个 template.sh,以后写新脚本直接复制改,既省事又不容易漏掉这些"安全带".
快速回顾
- 函数:用
name() { ... }定义,调用不带括号;内部参数同样是$1 $2 - 函数返回:
return只能给 0–255 退出码,返回数据要靠echo+$(...)捕获;临时变量加local - 位置参数:
$1取值 ·$#个数 ·"$@"转发参数(几乎总用它,别用$*) ·$?退出码 - 默认值:
${1:-默认}让缺参数时不崩;$# -lt N做参数校验 - 退出码:
exit 0成功,非 0 失败,是脚本向上层报告成败的唯一信号 - 严格模式
set -euo pipefail:出错即停 + 未定义变量报错 + 管道任一环失败即失败,生产脚本标配 - 允许失败:用
|| true或放进if;trap cleanup EXIT 保证退出时清理临时文件/锁
动手练习
- 函数与 local:写一个
add()函数接收两个数字,用echo返回和,在主程序用$(...)捕获并打印;给函数内变量加local验证作用域 - $@ vs $*:写一个脚本打印
$0 $# $@,用不同数量,含空格的参数运行,观察"$@"和"$*"的区别 - 默认值:用
${1:-default}给脚本第一个参数设默认值,不传参数时验证默认生效 - set -u 验证:在脚本开头加
set -euo pipefail,故意引用一个拼错的变量名,观察-u如何报错退出 - pipefail 对比:写一个
cat 不存在的文件 | grep foo的管道,分别在有/无pipefail时用echo $?看退出码差异 - trap 清理:用
trap cleanup EXIT注册清理函数删除一个mktemp临时文件,然后让脚本中途exit 1,确认临时文件仍被删除 - 脚本骨架:套用本篇的"健壮脚本骨架"写一个真实小脚本(如统计某目录下文件数),把它存成自己的
template.sh