Shell 编程中的 test, [ 和 [[

2023-11-26 ⏳3.0分钟(1.2千字)

写过 Shell 的朋友一定摆脱不了被方括号支配的恐惧,Shell 分支判断语法简直是奇葩。时而用[,时而用 [[,甚至有时还会用 test,跟常见的编程语言差距太大。我自己也没弄明白为什么要分单括号和双括号,直到我遇到这篇文章1。今天就结合原文以及自己的理解为大家梳理一下 Shell 中的方括号语法。

先来一段 Shell 脚本:

a=1

if [ $a = 1 ]; then
  echo yes
else
  echo no
fi

执行上面的脚本会输出yes。如果我们去掉第一行的赋值语句a=1,再执行会输出no,但同时还会一行报错:

sh: [: =: unary operator expected

这是为什么呢?如果换而双括号再试试:

if [[ $a = 1 ]]; then
  echo yes
else
  echo no
fi

就不会有报错,直接输出no。当然了,还有一种奇技淫巧可以让单括号版本的比较也不报错,那就是给变量$a加上双引号,改为"$a" = 1。这又是什么原理呢?

此事从根上说就得刨到 UNIX 的基本哲学:一次只做一件事,并把它做到极致

在 UNIX 上,最早期的 Shell 分支语法如下:

if 执行某命令; then
  # 如果命令执行后返回码(也就是 $? 的值)为 0
  # 则继续执行本分枝内的指令
fi

所以我们可以写这样的脚本:

if grep hello a.txt > /dev/null; then
  echo find hello
fi

我们还可以写的再复杂一点:

if grep hello a.txt | grep hi > /dev/null; then
  echo find hello and hi
fi

这里if;之间可以是任意 Shell 命令的组合。if 只根据最后的状态码决定要执行哪个分支。那怎样比较两个变量呢?UNIX 里有专门的命令叫 test,该命令支持多种比较操作:

test s1 = s2   # 比较字符串
test n1 -eq n2 # 比较整数
test -z string # 判断是为空字符串
test -f path   # 判断文件是否存在

如果比较成功,$?返回值就是0,否则为非零。所以前面的数字比较可以改写为:

a=1
if test a = 1; else
  echo yes
fi

虽然还是在比较是否相等,但这次执行的是 test 这个程序。整个判断过程跟前面的 grep 没有任何区别。

为了让分支判看起来更美观,前人又搞出一个种序叫 [。对,你没看错,[grep 一样也是普通的可执行文件,路径为 /usr/bin/[

功能几乎跟 test 一样,便有一点小区别,[ 要求最后额外加一个参数 ]。这个参数纯属装饰,没有实际作用。但如果不传就会报错。前面的 test 判断可以改写成:

[ s1 = s2   ] # 比较字符串
[ n1 -eq n2 ] # 比较整数
[ -z string ] # 判断是为空字符串
[ -f path   ] # 判断文件是否存在

对于 [ s1 = s2 ] 而言,[ 是命令,后面接收四个参数s1,=,s2,]。最后的参数]没有实际意义。前三个有用,分别是比较参数一、比较运算符、比较参数二。

如果参数不够,就会报错。

[ = 1 ]
ash: 1: unknown operand

回到最上的的例子:

if [ $a = 1 ]; then
  echo yes
fi

如果脚本没有给变量$a赋值,那么上面的比较就会变成 [ = 1 ],少了一个操作数,自然就会报错。如果改成加引号的版本,则比较命令就会变成 [ "" = 1 ],仍然有四个参数,所以不会报错。

到现在我们就解释了 [ 相关的奇技淫巧。那 [[ 又是什么鬼呢?

我们前面讲过,传统 Shell 的比较会起新的进程运行对应的命令,再根据返回值判断。但每次都起新的进程太耗资源,所以后来的诸如 Bash/Zsh 等新兴的 Shell 都提供了内置的比较运算符,也叫 builtin。所以内置,就是不需要依赖外部的 test 或者 [ 命令,而是由 Shell 在解释执行的时候直接比较。

比如上面的

if [[ $a = 1 ]]; then
  echo yes
else
  echo no
fi

如果由 Bash 执行,它会把 if [[ $a = 1 ]]; then ... then ... fi 当成一个整体。当 Bash 看到 if [[ $a = 1 ]]; 的时候会直接计算比较结果。因为 Bash 自己知道变量 $a 到底有没有定义,所以可以确定 $a = 1 的结果。所以使用 [[ 不需要在变量上加双引号。

虽然 [[ 在性能上有优势,但可能会有兼容性问题。而且在功能上也相对不灵活。如果是用 [ 命令,我们甚至能写出 if [ $a = 1 ] && grep hi a.txt; then ... fi 这样的脚本,确实比较黑科技。但无论如何,很多事情表面看似复杂,实则是我们没有理解它简单的内核。Keep it simple, stupid!


  1. https://jmmv.dev/2020/03/test-bracket.html↩︎