Shell 里的冒号什么都不做,但你还是该用它
文章摘要
作者 Filip Roséen(HN 上的 refp)写道:他写过的 shell 脚本多到数不清,但仍然会时不时被一些技巧震住,最近这个技巧是 shell 里的冒号。
文章从一个所有人都写过的场景开始:脚本需要必填参数,于是你条件反射地写一个 if 判断——参数为空就往 stderr 打一句「missing argument, aborting.」然后 exit 1。四行。作者说这四行可以压缩成一行:
: "${1:?missing argument, aborting.}"
echo "Hello $1!"
不带参数运行时 bash 直接报错退出;带参数运行则正常打印。如果把位置参数换成一个有名字的变量(比如 GREET_NAME),诊断信息里还会自动带上变量名,更容易定位。
这一行里其实有两个东西在起作用。第一个是参数展开:${name:?diagnostic} 会检查 $name 是否未设置或为空,是的话把 diagnostic 打到 stderr 并以非零状态退出 shell;否则等价于 $name。第二个才是主角——行首那个孤零零的冒号。: 是空命令(null-command),一个内建命令,它什么都不做,只负责求值自己的参数然后把结果丢掉。作者特意提到它的年龄:: 可以一路追溯到 1971 年的 Thompson shell,在那里它兼作标签,同时也是 Unix 最早的注释标记。
文章后半段给出了一串「冒号高光时刻」的例子:用 : "${DATA_DIR:=/var/data}" 设置默认值(冒号把展开结果吞掉,而不是拿去当命令执行);: > error.log 截断文件,甚至 : > error.log > access.log 一次截断两个;( : < dataset.json ) && echo YES 测试文件是否可读、( : >> result.json ) && echo YES 测试是否可写;trap : INT 因为 trap 必须要有一个命令;在 set -u 之后用 : "$DEPLOY_ENV" "$HOST" 来集中检查必需变量;以及在 if 分支里当占位符用,因为那个位置语法上必须有命令。
因为发布后收到不少质疑,作者补了一节 FAQ。为什么需要空命令、展开难道不是本来就会发生吗?——展开确实会发生,但没有空命令的话 shell 会把展开结果当成命令去执行,${HELLO:=123} 会得到「command not found: 123」。为什么不直接写 VAR=${VAR:-default}?——核心是个人偏好,但用冒号的写法变量名只出现一次,可能打错的地方从两处缩到一处。为什么要用这种伤害可读性的写法?——作者澄清了最常见的误读:那个 if 分支的例子当然可以用 if ! some-command 消掉,但那不是重点;这个片段要表达的不是「有 if 语句就该用空命令」,而是「当我处在一个语法上要求有命令、但我什么都不想做的上下文里,可以用空命令把它变成 no-op」。
文章末尾还收录了一个来自 HN 评论的真实用法:用户 fphilipe 把 : 当作 git 的 EDITOR,从而在做 interactive rebase + autosquash 时不必真的去编辑那个 todo 列表,他为此起了个别名叫「快速 interactive rebase」。
HN 评论精华
这条讨论最鲜明的特征是:作者本人几乎回复了每一条批评,而讨论区的多数意见其实是反对在生产代码里使用这些技巧。
「太不可读了」是压倒性的主流意见
- archargelod:「为什么要把一个完全可读的 if 语句变成 99.9% 的人都得去查文档的东西?简洁 ≠ 更好。」他还给了一个单行替代写法。
- PunchyHamster:「我会拒绝这个 PR。Bash 作为编程语言本来就够糟了(一门语言对长代码的友好程度,与它写 shell 单行命令的爽快程度成反比),这只是把『糟』变成『线噪音』。如果你的 bash 脚本超过一屏,用 Python 重写,见鬼,哪怕用 Perl 重写都更好。」
- lucideer:读完文章里所有例子,他的结论是它们唯一的作用就是把可读的多行代码变成单行;单行命令在早期 shell 文化里是个有趣的产物、今天在复制粘贴快捷命令时偶尔还有用,但「它们在脚本里没有位置」。对于作者那句「如果你和我一样喜欢少打字(要快)」,他的回应是三个字:「Yeah, no.」
- jeffrallen:「谢谢你写这篇。另外,我永远不会用它。因为一个需要营销的语言特性,对于还没读过这份营销材料的目标读者来说,就是反可读性的。」
- dtj1123 给出了本串最狠的一句:「我对 bash 怪技巧深恶痛绝。脚本作者觉得自己聪明高效的程度,恰好等于未来读这个脚本的人觉得自己是白痴的程度。」
- zaptheimpaler(34 条子回复的起点)走得更远:「人生太短,不值得为超过两行的脚本去应付这门语言和它的五万个走火装置,尤其在 LLM 时代。写个 Python/TS/任何真正的语言的脚本就行了。Bash 适合命令行,就该限制在命令行。」jghn 的回复反转了这个论证:「结果发现 LLM 写 bash 也很好,甚至 perl 也行!也许我们该重新考虑这些失落的技艺,因为我们不再需要操心那些晦涩的部分了。」
技术性纠错与作者回应
- kazinator 认为
( : < dataset.json ) && echo YES里的子 shell 括号和冒号都是多余的,直接< dataset.json && echo YES即可,重定向不需要挂在冒号命令上。作者 refp 给出了反驳并附上 zsh 实测:在 zsh 里< data && echo READABLE会把文件内容打印出来,而: < data && echo READABLE不会——所以如果你要写「所有人都能用」的东西,就用空命令。kazinator 还对可写性测试提出了实务质疑:文件不存在的话你会创建一个零长度文件;如果本来就打算覆盖它,为什么不直接> result.json?他说自己从没写过这种测试,通常就直接执行写操作,让它失败。 - jiveturkey 认为截断的例子是个糟糕的示范:它在文件不存在时是创建而非截断,作为教学博客这个描述不完整;而且不加冒号也一样能工作;真正有教育价值的是「重定向和参数展开一样,在命令执行之前就发生了」,而文章完全没解释这一点。作者承认,并说截断和可写性测试那两个片段本来就是半开玩笑的,「我没指望有人真在生产环境用这些,我只是想展示一个被设计成什么都不做的命令能做到什么(疯了)。」
- kevincox(最高赞):他不太喜欢大部分技巧,但少数确实有用。对于必填参数,他仍会用赋值以便后续有个名字;不过用来给必需的环境变量一个清晰的报错是不错的。截断文件这类还是用专门命令更清楚,除非你在为「避免 shell 之外的任何依赖」而走极端。作者据此更新了文章的第一个例子。
- amiga386 提到那个
if ... then : else ...的写法他以前也用,直到学会if ! some-command,并指出!在 POSIX 标准里,不是 bashism。 - normie3000 贡献了本串被作者点赞的最佳笑话:「冒号是 shell 的阑尾。」
真正在用的人
- olexsmir:常用
:给配置类环境变量设默认值,比如 dotfiles 引导脚本里的: "${DOTFILES_PATH:=$HOME/.dotfiles}"。 - fphilipe 贡献了被作者收进正文的那个用法:把
:当作 git 的 sequence editor 来做免编辑的 autosquash rebase。作者的反应是:「厉害,我自己肯定要用这个(或它的变体)!」 - rgrau:喜欢这类怪技巧,但指出很多例子展示的其实是参数替换而非冒号本身。他自己在小作用域里会把
:?校验内联进命令参数,另一个用法是把工作放在 while 的条件里、循环体只放一个冒号,从而得到「成功就继续做 X,返回非零就停」的语义。 - m2f2 展示了用
while : ; do case ... esac done手写参数解析来替代 getopt,但他也说在if/else里用:就过头了;此外「在 Linux/Unix 上用 PowerShell?对管理上千台机器的人来说只是又一个巨大依赖,而且祝你能找到愿意不戴化学级手套碰 pwsh 的 Linux 工程师。」 - c0l0 分享了自己几年前写的一篇文章,介绍他最爱的冒号用法(用于调试输出)。
PowerShell 支线
IdiotSavage 借机安利:「如果有一种更不晦涩、更难写错的方式来声明必填参数呢?」并展示了 PowerShell 的 [parameter(mandatory)] 声明——缺参数时交互式会提示你补,非交互式会给出清楚的错误。作者的反应相当热情:他前几天刚写了人生第一个真正的 .ps1,「PowerShell 感觉像是我从未拥有过的情人。顶部那些可读注释就能生成可用文档?该死、该死、该死。我心里有一部分想拿它当日常主力用两周。」但他补充自己是 zsh vi-mode 党,「一直都是,也永远会是」。delta_p_delta_x 补充了 PowerShell 被严重低估的一点:它跑在 CLR 上,能直接访问 .NET 对象和类型,因而能用上 P/Invoke 直至 Windows API。garethrowlands 泼冷水:「我看不出 pwsh 能在这个生态里找到位置。连 oil 和 fish 都很挣扎。那个位置基本被 Python 占了。」
关于 shell 本身的两句总结
garethrowlands 在多处给出了同一个平衡观点:客观地说 POSIX shell 语法确实糟糕,尽管它促成的整个系统很了不起;历史对它宽容是因为它解决了当时的问题——但 awk 年纪相仿,它的存在本身就证明了 shell 语法并非非得犯这些错。wpm 则为 shell 辩护:「它不是『不可读』的语法,它是简洁而陌生的语法。很多语言的语法对我来说都陌生难读,只是因为我不熟。事实上大多数编程语言第一眼都不可读,也许除了……AppleScript?也许 Scratch。连 Python 著名的可读性也不是保证的,它那些函数式编程设施跟 *sh 一样晦涩。我 50% 的工作时间在 shell 里,这篇文章里我没觉得有任何东西不可读。」
最后是 luciana1u 的一句妙评:「冒号什么都不做,这让它成为唯一一个 LLM 无法过度设计的 bash 命令。」