一个冒号能干什么?在大多数编程语言里它只是标点,但在 shell 里,单独一个 : 是一条内建命令——什么都不做,只把参数展开一遍,再把结果扔掉。听起来像个冷笑话,但正是这个“啥都不做”的命令,能把四行参数校验代码压成一行,把默认值设置、文件权限检查也一并接管。真正值得琢磨的不是省了几行代码,而是它把 shell 里的求值执行这两件事拆开了——这是老练的 Unix 思路,也是新手最容易踩的坑。

速读:一个冒号怎么干四件事

  • 必填参数校验.: "${1:?missing argument, aborting.}" 一行顶替四行 if-then-exit;变量为空或未设置,直接向 stderr 打印诊断信息并以非零状态退出。
  • 默认值.: "${DATA_DIR:=/var/data}",冒号丢掉展开结果,但 := 会把值真的赋给变量,这个副作用留下来了。注意它跟 ${VAR:-default} 不是一回事:后者只在这一处替代成默认值,不会改变变量本身。
  • 文件检查.(: < dataset.json) && echo YES,借重定向去测试文件能不能读,冒号本身根本没碰文件。
  • 语法占位.trap : INTif cmd; then : else ...,需要一个命令占位但什么都不用做时,冒号就是那个占位符。

受益的是写 POSIX/Unix shell 脚本的人,尤其是维护 CI、部署、运维脚本的工程师——脚本行数、可读性和出错代价,是这类场景天天要打的架。

拆解:不执行,但展开照常发生

关键是分清求值和执行。${VAR:?diagnostic}${VAR:=default} 这类参数展开,shell 本来就会做,跟前面挂不挂冒号没关系。挂冒号只是给展开结果一个安全的落点——被读、被丢弃,但不会被误当成命令去执行。

如果不挂冒号,比如直接写 ${HELLO:=123},shell 展开完会拿着结果去找一个叫 123 的命令来运行,大概率报 command not found。这也是为什么很多人第一次看到会问“不加冒号不是也会展开吗”——展开确实照常发生,但没人接着,展开完的字符串就被当命令扔出去了。

展开之后,谁接住结果 没有冒号接管 ${HELLO:=123} 展开为 123,被当命令跑 command not found 冒号接住结果 : ${HELLO:=123} 展开为 123,结果被丢弃 HELLO=123 保留

这个空命令的历史比大多数人想的老。它一路能追到 1971 年的 Thompson shell,当时还兼职当标签和 Unix 最早的注释符——一个符号被反复征用几十年,本身就说明它够简单、够好用。


锐评:省的是代码,欠的是可读性

真正牛的地方不是省代码,是把参数展开的副作用和命令执行彻底拆开,靠这一点就能把校验、赋默认值、测文件权限统一到一套语法里。这是值得学的 Unix 思路。

但省字数和欠可读性是同一枚硬币的两面。拿默认值写法对比一下:

变量名写几遍,风险差多少 1 次提及变量名 冒号写法 2 次提及变量名 传统赋值写法

变量名少写一遍,拼错的机会就少一半,这是实打实的收益。但省下的这几个字符,账不会白给:

  • 结论.: 的真正价值是把求值和执行拆开,值得写脚本的人花十分钟学一次,不是为了炫技,是为了少写样板代码。
  • 风险.团队里没人真的分得清 :=:-,或者不熟悉这套语义,这种写法省的代码,迟早要靠下一个改脚本的人加班读懂。
省下的三行代码,账最后落在读脚本的人头上。

也别把所有 shell 的报错文本、退出码细节想成完全一致——上面例子是按 POSIX 语义讲的,具体到 bash、zsh、dash,报错措辞和一些边界行为会有差异,写跨 shell 的脚本前最好自己核实一遍。

回到开头,一个冒号确实什么都没做。但它让 shell 脚本少了一堆自己写的样板代码,也把判断力从“会不会写 if”移到了“看不看得懂 ${VAR:?} 这一串符号”。这笔账划不划算,取决于接手这份脚本的下一个人,到底熟不熟 shell。