其实本文不是介绍 条件分支语法 的, 只介绍测试(test)语法,也就是说:本文只介绍如何写条件语法中的表达式。 其实本文介绍的内容也并未针对 Bash,多数 Shell 都是适用的。

与其他编程语言类似,if 的条件可以接受任何 表达式,计算为真就进入分支。 只是在 Bash 中表达式是一个命令调用

真与假

在 POSIX 标准下,程序的 退出状态 为零表示成功,非零表示失败。 而 Bash 的 表达式 就是命令调用,因此 0 为真(命令调用成功)1 为假(命令调用失败)。

> true
> echo $?   # 查看上一个命令的退出状态
0
> false
> echo $?
1

有些 Shell 中 true 是 built-in 命令,有些 Shell 中是 /usr/bin/true 二进制文件,但它们都会给出退出码零。

() subshell

如果命令很复杂可能会破坏 if 的结构,这时可以通过 ( expression ) 来强制在 subshell 中执行。 其中 ()Shell 特殊字符。 没错,跟管道一样这会开一个 subshell。这里提一下 subshell 和子进程的区别: subshell 是一种特殊的子进程,可以访问父进程的任何变量(不需要 export)。下面的例子来自 wooledge.org

unset a; a=1
(echo "a is $a in the subshell")            # a is 1 in the subshell
sh -c 'echo "a is $a in the child shell"'   # a is  in the child shell

但 subshell 仍然是一种子进程,这意味着 subshell 中环境变量的定义和赋值对父 shell 是不可见的。 这一点是 subshell 的主要用途。

test 命令

test 命令可以用来测试文件是否存在、文件类型,也可以进行字符串判等、数字比较等操作。 在测试为真时 test 命令的退出码为 0,测试为假时退出码为 1,发生错误时大于 1。

使用示例

多数 Shell 实现中它们都属于 built-in 命令,它有两种使用方式:

test expression
[ expression ]

例如:

if test -e harttle.png; then
    echo file exists
fi

等价于:

if [ -e harttle.png ]; then
    echo file exists
fi

注意第二种用法中 [ 是一个命令,expression] 是它的两个参数。 因此如果 [ 后没有空格会发生 command not found 错误。

常用参数

下面介绍常见的几种用法,完整列表请 man testman [

文件判断

可以检查文件是否存在,文件类型,权限状态等。下面常见的文件判断参数:

-e file: file 存在
-d file: file 是一个目录
-f file: file 是一个普通文件(regular file)
-s file: file 非空(大小不为零)
-w file: file 是否有可写标志
-x file: file 是否有可执行标志

字符串判断

主要包括字符串长度判断,和字符串判等两类:

-n string: 字符串长度非零
-z string: 字符串长度为零
string1 = string2: 字符串相等
string1 != string2: 字符串不相等
string: 字符串不是null时为真,否则为假

数字判断

数字测试参数包括:

num1 -eq num2: 等于
num1 -ne num2: 不等于
num1 -le num2: 小于等于
num1 -lt num2: 小于
num1 -ge num2: 大于等于
num1 -gt num2: 大于

这里有一个陷阱,既然 num1, -gt, num2 都是以参数的形式传递给 test 命令的, 那么如果参数替换的结果是空,那么就会因为缺少参数而崩掉。例如:

> a=
> test 1 -eq $a
zsh: parse error: condition expected: 1

所以需要引号包裹所有变量来保证参数总是存在的,字符串判断也需要一样的处理。

> a=
> test 1 -eq "$a"
> echo $?
1

逻辑运算

当然,与(-a)或(-o)非(!)都是支持的。但括号要转义,因为括号是 Shell 特殊字符,用来开启子 Shell 的。 一个复杂的例子如下:

if [ ! \( -f harttle.png \) ]; then
    echo file not found
fi

需要强调的仍然是注意空格!即使 \( 也是需要单独一个参数传递给 [ 命令。 下面的代码会产生错误:[: (-f: unary operator expected, 因为 ! 是单目运算符(Unary Operator),但后面跟着的是两个参数:\(-fharttle.png\)

if [ ! \(-f harttle.png\) ]; then
    echo file not found
fi

[[ 条件表达式

可以把 [[ expression ]] 看做是 [ 的现代版本,它与 test 命令有几乎一样的参数。 但它是一个新的语法结构,不再是一个普通的 test 命令,因此不受制与 Shell 的 参数展开,比如 不需要用引号包裹所有变量,也支持类似 &&|| 这样的逻辑操作而不需要用类似 -a-o 这样的参数。 例如:

if [[ $string1 == 'harttle' ]]; then
    echo The author for this article is harttle
fi

[[ 语法还支持正则匹配,文章评论中有具体的例子,感谢 yantze。 详细的文档见 https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html#Conditional-Constructs

算术展开(Arithmetic Expansion)

算术展开 的语法是 $((expression)),这是从其他编程语言来的人最顺手的算术操作方式。与 test 命令相比,

  • C 风格的算术操作语法
  • Shell 会把 expression 内容的每一项都当做就像被引号包裹了一样。
  • 会依次进行参数展开命令替换,和 引号移除

因此可以不需要像 test 命令那样用引号包裹所有变量,也不需要注意空格,也可以写 C 风格的操作:

> a=2
> echo $(($a+3))
5

这只是一种 shell 展开并没有 subshell,因此在括号内进行的赋值会直接反映到当前的变量上。 需要注意的是 C 风格的判等 1 为 True,0 为 False。因此用作 if 命令的条件时需要反过来:

if test $(( 1 == 1 )) -eq 1; then
    echo one equals one
fi

是不是很变态?是的。因为 C 风格与 Shell 风格就是不同,下面介绍的 [[ 条件表达式 更加 Bash 风格,用起来表现地更一致。

总结

  • Shell 中的 if 通过命令的退出码来确定成功与否,而成功(True)的退出码是零。
  • test 是最基本的 Shell 风格的判断命令,它是一个命令所以要命令行参数的方式去使用,要小心参数替换
  • [[ expression ]] 是现代版的 test,是一个语法构造而非命令。因此在传参上可以舒服很多。
  • 如果你是来自 C 的程序员,算术展开 会很适合你进行任何算术操作,包括条件判断。

需要注意的是,所有空格仍然需要保留,因为它仍然依赖 Bash 的词法解析。 更多使用方式请参考 Bash Hackers Wiki

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2018/10/23/bash-test.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。