一个冒号能干什么?在大多数编程语言里它只是标点,但在 shell 里,单独一个 : 是一条内建命令——什么都不做,只把参数展开一遍,再把结果扔掉。听起来像个冷笑话,但正是这个“啥都不做”的命令,能把四行参数校验代码压成一行,把默认值设置、文件权限检查也一并接管。真正值得琢磨的不是省了几行代码,而是它把 shell 里的求值和执行这两件事拆开了——这是老练的 Unix 思路,也是新手最容易踩的坑。
速读:一个冒号怎么干四件事
- 必填参数校验.
: "${1:?missing argument, aborting.}"一行顶替四行 if-then-exit;变量为空或未设置,直接向 stderr 打印诊断信息并以非零状态退出。 - 默认值.
: "${DATA_DIR:=/var/data}",冒号丢掉展开结果,但:=会把值真的赋给变量,这个副作用留下来了。注意它跟${VAR:-default}不是一回事:后者只在这一处替代成默认值,不会改变变量本身。 - 文件检查.
(: < dataset.json) && echo YES,借重定向去测试文件能不能读,冒号本身根本没碰文件。 - 语法占位.
trap : INT、if cmd; then : else ...,需要一个命令占位但什么都不用做时,冒号就是那个占位符。
受益的是写 POSIX/Unix shell 脚本的人,尤其是维护 CI、部署、运维脚本的工程师——脚本行数、可读性和出错代价,是这类场景天天要打的架。
拆解:不执行,但展开照常发生
关键是分清求值和执行。${VAR:?diagnostic}、${VAR:=default} 这类参数展开,shell 本来就会做,跟前面挂不挂冒号没关系。挂冒号只是给展开结果一个安全的落点——被读、被丢弃,但不会被误当成命令去执行。
如果不挂冒号,比如直接写 ${HELLO:=123},shell 展开完会拿着结果去找一个叫 123 的命令来运行,大概率报 command not found。这也是为什么很多人第一次看到会问“不加冒号不是也会展开吗”——展开确实照常发生,但没人接着,展开完的字符串就被当命令扔出去了。
这个空命令的历史比大多数人想的老。它一路能追到 1971 年的 Thompson shell,当时还兼职当标签和 Unix 最早的注释符——一个符号被反复征用几十年,本身就说明它够简单、够好用。
锐评:省的是代码,欠的是可读性
真正牛的地方不是省代码,是把参数展开的副作用和命令执行彻底拆开,靠这一点就能把校验、赋默认值、测文件权限统一到一套语法里。这是值得学的 Unix 思路。
但省字数和欠可读性是同一枚硬币的两面。拿默认值写法对比一下:
变量名少写一遍,拼错的机会就少一半,这是实打实的收益。但省下的这几个字符,账不会白给:
- 结论.
:的真正价值是把求值和执行拆开,值得写脚本的人花十分钟学一次,不是为了炫技,是为了少写样板代码。 - 风险.团队里没人真的分得清
:=和:-,或者不熟悉这套语义,这种写法省的代码,迟早要靠下一个改脚本的人加班读懂。
省下的三行代码,账最后落在读脚本的人头上。
也别把所有 shell 的报错文本、退出码细节想成完全一致——上面例子是按 POSIX 语义讲的,具体到 bash、zsh、dash,报错措辞和一些边界行为会有差异,写跨 shell 的脚本前最好自己核实一遍。
回到开头,一个冒号确实什么都没做。但它让 shell 脚本少了一堆自己写的样板代码,也把判断力从“会不会写 if”移到了“看不看得懂 ${VAR:?} 这一串符号”。这笔账划不划算,取决于接手这份脚本的下一个人,到底熟不熟 shell。
