Shell 中的冒号(colon)无所作为。但尽管如此,还是用它吧
Shell 中的冒号(colon)无所作为。但尽管如此,还是用它吧
引言
Shell 中的冒号 (:) 是一个内置的空命令(null-command),它会评估其参数并丢弃结果。它本身不执行任何操作,但当它置于参数扩展(parameter expansion)之前时,它允许扩展发生,而不会让 Shell 尝试将生成的字符串作为命令执行。
验证必需参数
使用 : ${VAR:?diagnostic} 扩展提供了一种单行方式来确保变量已设置且非空。
: "${1:?missing argument, aborting.}"
echo "Hello $1!"
如果 $1 未设置或为空,Shell 会将诊断信息打印到 stderr 并以非零状态退出;否则,扩展会产生 $1 的值,脚本继续执行。相同的技术也适用于命名变量,例如 : "${GREET_NAME:?missing argument, aborting.}",它会在错误消息中包含变量名。
使用 := 设置默认值
${VAR:=default} 扩展在 VAR 未设置或为空时分配默认值。在前面加上 : 会在执行赋值的同时丢弃分配的值。
: "${DATA_DIR:=/var/data}"
: "${RETRIES:=3}"
执行这些行后,如果 DATA_DIR 和 RETRIES 之前未设置,它们将持有默认值。
截断和测试文件
在 : 之后的重定向会将文件截断为零长度,因为空命令不产生任何输出。
: > error.log # 截断 error.log
: > error.log > access.log # 同时截断两个文件
类似地,仅包含重定向和 : 的子 shell 可用于测试可读性或可写性。
( : < dataset.json ) && echo YES # dataset.json 是否可读?
( : >> result.json ) && echo YES # result.json 是否可写?
之所以需要冒号,仅仅是因为子 shell 需要一个命令;重定向本身完成了工作。
在 trap 和条件语句中使用冒号
trap 内置命令需要一个命令;: 满足了这一要求且不执行任何操作。
trap : INT
sleep 60 # sleep 保持可中断状态
在 if-else 结构中,如果 then 分支必须包含一个命令,: 可以作为占位符。
if some-command; then : # 需要命令 else echo "command failed" fi
常见问题解答(摘自文章)
为什么我需要这个空命令?没有冒号扩展不也会发生吗?
参数扩展无论如何都会发生,但如果没有空命令或类似的结构,Shell 会将生成的字符串视为要运行的命令。例如:
% ${HELLO:=123} zsh: command not found: 123
在前面加上 : 会在执行扩展的同时丢弃结果:
% : ${HELLO:=123} % echo $HELLO 123
既然可以用 VAR=${VAR:-default-value},为什么要使用空命令?
两者达到相同的效果,但冒号形式只需提及一次变量,减少了拼写错误的概率:
: "${DATA_DIR:=/var/data}" # DATA_DIR 只出现一次 DATA_DIR="${DATA_DRI:-/var/data}" # 可能出现拼写错误 (DATA_DRI)
社区反应(精选 HN 评论)
@gpvos 回想起了 Larry Wall 的语言重新设计法则:“每个人都想要冒号。Larry 得到了冒号。”
@amiga386 注意到了
if some-command; then : else echo "command failed" fi这一惯用法,并指出等效的if! some-command; then echo "command failed" fi也是符合 POSIX 标准的。@rgrau 建议直接在命令参数中内联
:?测试,例如echo "${1:? first param required}",并描述了一种在循环体中使用冒号的 while 循环模式。@fphilipe 将冒号用作 Git 的
EDITOR,以便在不打开编辑器的情况下执行带有auto-squash的交互式 rebase。@archargelod 认为用
: "${1:?...}"替换可读的if语句会损害可读性,并提出了另一种选择[ -z "$1" ] && { echo "missing argument, aborting." 1>&2; exit 1 }。@kevincox 发现
:?形式对于检查必需的环境变量很有用,但认为许多其他示例使用专门的命令会更清晰。@kazinator 批评子 shell 测试
( : < file ) && echo YES是多余的,指出仅靠重定向就可以测试可读性/可写性,并且使用: >> file创建零长度文件可能并不理想。@olexsmir 分享了一个个人使用案例:在 dotfiles bootstrap 脚本中使用
: "${DOTFILES_PATH:=$HOME/.dotfiles}"为配置环境变量设置默认值。@luciana1u 评论说冒号无所作为,这使得它成为“LLM 无法过度设计的唯一 Bash 命令”。
@m2f2 描述了在 getopts 风格的循环中使用冒号的情况:
while : ; do case "$1" in... esac done。@jeffrallen 感谢了作者,但表示他不会使用这种惯用法,因为他更喜欢能向受众解释清楚的代码。
@lucideer 觉得这些示例只是将可读的多行代码变成了单行代码,他认为这在脚本中没有地位。
@normie3000 相比
if! x; then something; fi更喜欢if x then :; else something; fi。@PunchyHamster 说他会拒绝引入这种单行代码的 pull request,主张用 Python 或 Perl 重写较长的脚本。
@garethrowlands 将这种技巧的需求归因于 Shell 语法中古老的字符串替换本质。
@ocd 使用冒号创建“无操作”别名,例如
: alias f3probe,以便快速 grep 遗忘的工具。@dominiwe 需要冒号来使一个 Shell 脚本同时成为一个有效的 YAML 文件。
@del 怀疑冒号是否是早期 Unix Shell 中少数几个特殊处理的代码路径之一。
@greatgib 说他第一次是在 Claude 生成的代码中看到这个结构,后来在文章中看到。
@search_facility 从 bash 转向了纯 Python 脚本以获得跨平台稳定性,并放弃了“bash zoo”。