Shell 冒号:什么都不做,但请尽管用
在遥远的地方.. #
一杯过凉的咖啡旁边放着一杯刚泡好的滚烫咖啡。四个终端窗口,其中三个一小时前就该关了,还有一个我实在(真的真的)不想写的 shell 脚本。
谁能想到,区区一个冒号竟能拯救这个夜晚?白天
注意:想立刻开开眼界?看看聚光灯下的更多冒号。
检查必需参数 #
这已经是老套路了,闭着眼睛都能写出来。你有一个脚本,需要几个参数,其中一些是必填的;好吧,像往常一样,一个 if 语句:
if [ -z "$1" ]; then
echo "missing argument, aborting." 1>&2
exit 1
fi
echo "Hello $1!"
不过……如果我说上面四行代码只需要一行就能搞定呢?
: "${1:?missing argument, aborting.}"
echo "Hello $1!"
$ bash example.sh
example.sh: line 1: 1: missing argument, aborting.
$ bash example.sh refp
Hello refp!
再看看如果我们用有意义的变量名会怎样 —— 行为与之前相同,但更容易定位问题;诊断信息会包含变量名!
: "${GREET_NAME:?missing argument, aborting.}"
echo "Hello $GREET_NAME!"
$ bash greet.sh
greet.sh: line 1: GREET_NAME: missing argument, aborting.
参数展开与 :? 的故事 #
前面的代码片段中涉及两件事,你正确识别出其中一部分是使用了参数展开:
语法
${name:?diagnostic}检查$name是否未设置或为空 —— 如果是,则诊断信息打印到 stderr,shell 以非零状态退出;否则,如果变量已设置,则等同于
$name。
另一个冒号…… #
这是一个冒号,那么另一个呢?那个独自坐在行首的冒号?
:是空命令——一个内建命令,只求值参数并丢弃结果,不做任何事。:历史悠久,最早可追溯到 1971 年的 Thompson shell,它同时充当标签和 Unix 的第一个注释标记。:就像黑暗中凝视你的双眼,充满爱意。
更多冒号的高光时刻 #
或许我们已经知道 : 并不像表面那么简单,但为了展示空命令的真正魔力,这里有几个让我大开眼界的用法。
: "${DATA_DIR:=/var/data}" # 设置默认值,: 吞掉结果
: "${RETRIES:=3}" # 而不是作为命令运行
: > error.log # 清空 error.log
: > error.log > access.log # 同时清空 error.log 和 access.log
( : < dataset.json ) && echo YES # 判断 dataset.json 是否可读?
( : >> result.json ) && echo YES # 判断 result.json 是否可写?
trap : INT # trap 需要一个命令
sleep 60 # sleep 可被中断
set -u # 未设置变量时报错
: "$DEPLOY_ENV" "$HOST" # 检查 DEPLOY_ENV 和 HOST
if some-command; then
: # 需要一个命令
else
echo "command failed"
fi
冒号结语 #
所以,如果你像我一样,喜欢少打字(要快)——那么空命令和参数扩展这对组合,值得在咖啡凉透之前好好研究一下。
还有……这难道不神奇吗?
set : : : : : : : : : : : : : : : : : : : : :
while : colons are more than "${1:?magic}"; do
echo "$*" && shift
done
注意:上述示例可在本地安全运行,试试看!
常见问题 #
看了网上的一些评论,我发现自己确实漏掉了一些值得解释的地方。我会随着问题的出现持续更新这一部分。
为什么需要空命令?没有冒号,扩展不也一样发生吗? #
参数扩展无论如何都会发生,但如果没有空命令或类似用法,Shell 会把结果字符串当作命令来执行。
% ${HELLO:=123} zsh: command not found: 123如果在参数扩展前面加上空命令,结果会被丢弃,但表达式仍然会被求值(将
HELLO设为123)。% : ${HELLO:=123} % echo $HELLO 123既然可以用
VAR=${VAR:-default-value},为什么还要用空命令? #这本质上是个人的偏好问题,但使用我们心爱的冒号,可以把潜在的拼写错误减少到一处(而不是两处):
: "${DATA_DIR:=/var/data}" # <- DATA_DIR 只出现一次 (1) DATA_DIR="${DATA_DRI:-/var/data}" # <- 哎呀,写错了 (2)既然会影响可读性,为什么还要用这些技巧? #
对于本文中那些非常刻意编造的例子,存在一些常见误解,比如质疑它们的实用性,或者是否真该用它们。
以下面这个为例。
if some-command; then
: # command required
else
echo "command failed"
fi
当然,你可以用 if ! some-command 去掉分支,但这不是这段代码的重点——它想说明的是,在需要一个命令但什么都不想做的地方,可以用空命令来占位。
它从来不是让你理解为 “哦,有 if 语句就应该用空命令”,而是让你理解为 “哦,在需要一个命令的上下文中,我可以用空命令来什么都不做”。
: 在实际场景中的巧妙用法 #
用户 ifphilipe 在 news.ycombinator.com 上 评论了以下内容, 这正好是需要一个完全不做任何事的命令的场景。
当我想要进行交互式变基并自动压缩,又不想编辑待办列表时,我会把冒号设为 Git 的 EDITOR。为此我创建了一个别名[1],叫快速交互式变基:
riq = -c sequence.editor=: rebase --interactive
这篇文章是否本可以用上面那个例子,而不是备受争议的 if 语句?如果我有那么聪明能想到它——是的……但这样能让人更好地理解 : 到底是什么吗?我强烈怀疑不行。