A shell colon does nothing. Use it anyway

A shell colon does nothing. Use it anyway

Shell 中的冒号什么都不做,但你还是应该用它

A shell colon does nothing. Use it anyway. I’ve written more shell scripts than I can count, but I still stumble upon tricks that honestly blow my mind far too often than I care to admit. Latest thing that blew my head clean off? The shell colon.

Shell 中的冒号什么都不做,但你还是应该用它。我写过的 Shell 脚本数不胜数,但依然经常会发现一些让我大开眼界的技巧,频率高到让我不好意思承认。最近让我彻底震惊的是什么?就是 Shell 中的冒号。


In a land far-far away..

在遥远的地方……

… there was once a far too cold cup of coffee next to a freshly brewed far-too-hot one. Four different terminals where three could have been closed an hour ago, and a shell script which I really (really) did not want to write. Who would have thought a single colon would be the one to save the day night?

……曾经有一杯凉透了的咖啡,旁边放着一杯刚冲好、烫得要命的咖啡。四个不同的终端窗口,其中三个其实一小时前就可以关掉了,还有一个我真的(真的)不想写的 Shell 脚本。谁能想到,一个简单的冒号竟然能成为拯救这个夜晚的关键?


Checking for required arguments

检查必需参数

This is a familiar dance, it’s pretty much muscle memory by this point. You have a script, it takes a few arguments, and some of them are mandatory; alright, an if-statement like so many times before:

这套流程大家都很熟悉,几乎已经成了肌肉记忆。你有一个脚本,它需要几个参数,其中一些是强制性的;好吧,像往常一样写个 if 语句:

if [ -z "$1" ]; then
  echo "missing argument, aborting!" 1>&2
  exit 1
fi
echo "Hello $1!"

Though.. what if I told you the above four lines could be replaced by just… one?

不过……如果我告诉你上面这四行代码可以只用一行来代替呢?

: "${1:?missing argument, aborting!}"
echo "Hello $1!"
$ bash example.sh
example.sh: line 3: 1: missing argument, aborting!
$ bash example.sh refp
Hello refp!

Parameter expansion and the story of :?

参数扩展与 :? 的故事

There are two things going on in the previous snippet, and you are correct in identifying that one part is using parameter expansion: The syntax ${name:?diagnostic} checks whether $name is unset or empty — if it is, the diagnostic is printed to stderr and the shell exits with a non-zero status, otherwise; if the variable is set, it is equivalent to $name.

在上面的代码片段中发生了两件事,你没看错,其中一部分使用了参数扩展:语法 ${name:?diagnostic} 会检查 $name 是否未设置或为空——如果是,则会将诊断信息打印到标准错误(stderr)并以非零状态退出 Shell;否则,如果变量已设置,它就等同于 $name


That.. other colon

那个……“另一个”冒号

So that’s one colon, but what about that other one, the one who sits alone at the beginning of the line? : is the null command — a builtin that does nothing but evaluate its arguments and discard the result. : is old — it goes all the way back to the 1971 Thompson shell where it doubled as a label and Unix’s very first comment marker. : two eyes staring at you in the dark, with love.

那是其中一个冒号,但另一个呢?就是那个独自坐在行首的冒号?: 是空命令(null command)——一个内置命令,除了评估其参数并丢弃结果外,什么都不做。: 很古老——它一直追溯到 1971 年的 Thompson shell,当时它既是标签,也是 Unix 的第一个注释标记。: 就像黑暗中注视着你的双眼,带着爱意。


Other colons worth remembering

其他值得记住的冒号用法

Perhaps we have already established that there is more to : than meets the eye, but to prove the real magic of the null-command — here are a few usages that blew my mind.

也许我们已经确定 : 的作用远不止表面看起来那么简单,为了证明空命令的真正魔力,这里有几个让我大开眼界的用法:

: "${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

Con-colon-sion

总结(Con-colon-sion)

So, if you are like me and prefer less typing (gotta go fast) — the null command and parameter expansion are a good pair to remember.

所以,如果你像我一样喜欢少打字(追求速度)——空命令和参数扩展是一对值得记住的好搭档。

while :; do
  echo "I love colons"
  sleep 1
done

Frequently Asked Questions

常见问题解答

Why do you need the null-command? Doesn’t the expansion happen without the colon? 为什么需要空命令?没有冒号扩展就不会发生吗?

The parameter expansion will happen regardless, but without a null-command or similar usage the shell will treat the resulting string as a command to run. 参数扩展无论如何都会发生,但如果没有空命令或类似用法,Shell 会将生成的字符串视为要运行的命令。

% ${HELLO:=123}
zsh: command not found: 123

If we prefix our parameter-expansion with the null-command, the result is discarded, but the expression is still evaluated (setting HELLO to 123). 如果我们给参数扩展加上空命令前缀,结果会被丢弃,但表达式仍然会被评估(将 HELLO 设置为 123)。

% : ${HELLO:=123}
% echo $HELLO
123

Why use the null-command when I could do VAR=${VAR:-default-value}? 既然可以用 VAR=${VAR:-default-value},为什么还要用空命令?

This at its core boils down to personal preference, but given that the below only states the variable name once, you are far less likely to make a typo on either side of the assignment operator which b0rks your script. 这归根结底是个人偏好问题,但考虑到下面的写法只写了一次变量名,你不太可能在赋值运算符的两侧出现拼写错误,从而导致脚本崩溃。

DATA_DIR="${DATA_DRI:-/var/data}" # <- 哎呀,拼错了
: "${DATA_DIR:=/var/data}"         # <- 更安全