第五章:Expressions and Arithmetic

Posted by lili on

Bash 提供了许多不同的方法来完成相同的任务,有些语法几乎相同,但实际上却完成了非常不同的任务。通常情况下,这只是几个特殊字符的差异。我们已经见过 ${VAR} 和 ${#VAR},其中第一个表达式返回变量的值,而第二个返回其字符串长度(“变量引用”见第31页)。或者 ${VAR[@]} 和 ${VAR[*]},它们在引号使用上的差异(“引号和空格”见第14页)。

其他 bash 习惯用法可能会让你感到疑惑:何时应该使用两个方括号,何时应该使用一个,甚至是否需要使用方括号?(( … )) 和 $(( … )) 有什么区别?通常,这些符号在各自的用法中都有一些共同的含义,暗示着语法背后某种程度上的合理性。有时,选择表达式更多是出于历史原因。让我们来看看是否能解释一些这些惯用的模式和算术表达式。

只支持整数

bash shell 仅支持整数运算。它的主要目的是用于计数:迭代次数、文件数量、以字节为单位的大小。如果你想要或需要浮点数计算呢?毕竟,现在 sleep 命令允许使用小数值:sleep 0.25 会休眠四分之一秒。如果你想要休眠四分之一秒的倍数呢?你可能希望编写 sleep $(( 6 * 0.25 )),但这不起作用。

最简单的解决方案是使用另一个程序(如 bc 或 awk)进行计算。例如,这里有一个名为 fp 的脚本,你可以将其放在 ~/bin 目录或其他 PATH 下(并赋予执行权限):

#!/bin/bash -
# fp - 提供浮点数计算,通过 awk
# 使用方法:fp "表达式"
awk "BEGIN { print $* }"

有了这个脚本,你就可以编写 sleep $(fp “6 * 0.25”) 来得到所需的浮点数计算。也许计算并不是由 bash 执行,但 bash 确实帮助你完成了计算任务。

算术运算

尽管 bash 主要是面向字符串的语言,但是每当你在 bash 中看到双括号时,这意味着正在进行算术运算——对整数进行算术运算,而不是对字符串。这一点你应该从使用双括号的 for 循环变体中已经熟悉了:

for ((i=0; i<size; i++))

请注意,在双括号内部使用变量名称时,我们不必在变量名称前面加上 $ 符号。在 bash 中使用双括号时都是如此。那么我们还在哪里可以看到双括号的使用呢?

首先,我们可以使用美元符号和双括号进行算术计算,为 shell 变量创建一个值,就像这样:

max=$(( intro + body + outro - 1 ))
median_loc=$((len / 2))

同样,请注意,在双括号内部使用变量时,它们不需要前面的美元符号。 其次,考虑一下双括号的这种用法:

if (( max - 3 > x * 4 )) ; then
    # 在此处执行某些操作
fi

这次我们使用双括号,但没有前导的美元符号。为什么?有什么不同之处?

在第一种情况下,对于变量赋值,我们想要表达式的值,因此与变量一样,美元符号表示我们想要这个值。在第二种情况下,if 语句,我们不使用美元符号,因为我们只需要真/假布尔值来做出决定。如果双括号内部的表达式(没有美元符号)的值为非零,则括号化表达式的返回状态为 0——在 bash 中被视为“true”。否则,返回状态为 1(在 bash 中为“false”)。

请注意,我们说“返回状态”?这是因为双括号没有美元符号时,在语法上,就像你执行一个或多个命令一样。它不返回一个值,你无法将其用于赋值给一个变量。但是,在某些情况下,你可以使用它来为变量赋一个新值,因为 bash 支持一些 C 语言风格的赋值运算符。以下是一些示例。这些是完整的 bash 语句,每行一个:

(( step++ ))
(( median_loc = len / 2 ))
(( dist *= 4 ))

每个语句都执行算术运算,但在每种情况下,也有一个值的赋值作为该计算的一部分进行。表达式不返回任何值,只返回返回状态——你可以在每个语句执行后检查 $? 变量中的返回状态。

你能够使用美元符号双括号语法来写出前面示例中的这三个计算吗?可能更熟悉的写法是:

step=$(( step + 1 ))
median_loc=$(( len / 2 ))
dist=$(( dist * 4 ))

我们不希望将 $((step++)) 单独放在一行上,因为该表达式将返回一个数值,然后 shell 将其视为要执行的命令的名称。如果 step++ 的计算结果为 3,那么 shell 随后将尝试寻找一个名为 3 的命令。

关于空格的提醒

在 bash 的变量赋值中,等号周围不允许有空格。对于变量赋值,在语法上,它必须全部是一个文本的“单词”。然而,在括号内部,空格是可以的,因为括号定义了该“单词”的边界。

现在只剩下一种算术变体了——可能是出于历史原因。你可以使用内置的 shell 命令 let 来起到类似于双括号但不带美元符号的作用。因此,比较下面的等效语句:

(( step++ ))
let "step++"

(( median_loc = len / 2 ))
let "median_loc = len / 2"

(( dist *= 4 ))
let "dist*=4"

但是要小心——如果你在 let 表达式周围没有使用引号(单引号或双引号),那么在该表达式中就不应该有任何空格了。 (我们示例中的第一个 let 不需要引号,但养成始终使用它们的好习惯是个好主意。)空格会将你的命令分成多个单词,而 let 只接受一个单词,因此如果有多于一个单词,就会产生语法错误。

无需括号

我们说过,bash 是一种面向字符串的语言,但是有一种例外情况。你可以这样声明一个变量为整数:declare -i MYVAR,一旦这样做了,你就可以执行算术运算来给它赋值,而无需使用双括号,也不需要在变量名前加上 $ 符号。下面是一个示例,一个名为 seesaw.sh 的脚本:

declare -i SEE
X=9
Y=3
SEE=X+Y    # 只有这一行会进行算术运算
SAW=X+Y    # 这只是一个字面字符串
SUM=$X+$Y  # 这是字符串连接
echo "SEE = $SEE"
echo "SAW = $SAW"
echo "SUM = $SUM"

如果你运行这些语句,你会看到 bash 主要是面向字符串的。SAW 和 SUM 的值是通过字符串操作形成的。只有 SEE 是通过算术运算赋值的:

$ bash seesaw.sh
SEE = 12
SAW = X+Y
SUM = 9+3
$

这表明你可以在不使用双括号的情况下进行算术运算——但我们通常会避免这样做,因为这需要你将要赋值的变量声明为整数。如果你忘记了 declare 声明语句,或者将这样的表达式赋给一个未声明为整数的变量,你不会收到任何错误消息,只会得到一个不希望的结果。

复合命令

你可能对在脚本中看到单独一行的单个命令非常熟悉。你可能也熟悉在 if 语句的条件中使用单个命令,以查看命令是否成功,并根据结果采取行动。如果你读过第 1 章,你也看到过“无-if”的 if 语句习惯用法。现在让我们来看看简单的单命令 if 语句,就像这样:

if cd $DIR ; then # Do something ...

但是下面这些呢:

if [ $DIR ]; then # Do something ...
if [[ $DIR ]]; then # Do something ...

为什么在这两行中使用方括号,而第一个示例中没有?有什么区别?一个括号和两个括号,应该使用哪一个,何时/为什么?

没有任何括号,正在执行一个命令(在我们的示例中是 cd)。该命令的成功或失败被返回为实际上是 if 用来在其决策分支之间判断是否为 true 或 false 的 true 或 false(如果有的话)。在 bash 中,你可以在 if 语句中放置一个完整的管道命令(例如,cmd | sort | wc)。管道中的最后一个命令的返回状态决定了 if 语句是 true 还是 false。(这可能掩盖了非常难以发现的错误;参见第 96 页的“非官方 bash 严格模式”中的 set -o pipefail。)

单个方括号语法实际上也是在运行一个命令,即 shell 内置的 test 命令。单个左方括号是一个 shell 内置的相同东西,即 test 命令,但有一个区别:必需的最终参数是 ]。双方括号语法在技术上是一个 bash 关键字,它表示一个复合命令,其行为非常相似,但不完全相同于单个方括号和 test 命令。

我们使用单个或双方括号语法来执行一些逻辑和比较,即条件表达式。我们用它们来检查事物的状态,比如文件是否存在或具有某些权限,或者一个 shell 变量是否具有值或为空。在 bash 手册页的“条件表达式”部分中,可以查看所有你可以进行的测试和检查的完整列表,并在help test中快速查看提醒。

我们之前的示例是检查 DIR 变量是否具有非空值。另一种写法是:

if [[ -n "$DIR" ]]; then ...

以查看值是否不为空,即具有非零长度。相反,要查看变量的值是否为零长度,即未设置或为空,请使用:

if [[ -z "$DIR" ]]; then ...

那么单括号和双括号测试之间有区别吗?只有一点点,但它们可能是显著的。 也许最大的区别是双括号语法支持一个额外的比较运算符 =~,它允许使用正则表达式:

if [[ "$FILE_NAME" =~ .*xyzz*.*jpg ]]; then ...

正则表达式 这是 bash 中唯一会发现正则表达式的地方!并且记住:不要将正则表达式放在引号中,否则你将按原样匹配这些字符,而不是作为正则表达式。

单括号和双括号之间的另一个区别更具风格,但它会影响可移植性。这两种形式做相同的事情:

if [[ $VAR == "literal" ]]; then ...
if [ $VAR = "literal" ]; then ...

在条件表达式中使用单个等号进行比较可能对 C 和 Java 程序员来说显得不自然,但在 bash 条件表达式中使用单个等号和双等号都意味着相同的事情。对于 POSIX 兼容性,单个等号在单括号语法中被认为是首选的(bash 手册页如此说)。

微妙的差异 在双方括号内,< 和 > 运算符是“按当前语言环境词典顺序比较”的,而 test(以及 [)是使用简单的 ASCII 排序进行比较。

在使用单方括号时,你可能还需要转义这些运算符(像这样:if [ $x > $y]),否则它们将被视为重定向。为什么呢?因为单方括号(就像 test 命令一样)是一个内置命令而不是关键字,所以 bash 将其视为运行命令——在运行命令时可以重定向 I/O。然而,当 bash 看到双方括号时,作为一个关键字,它知道要期望这样的运算符,并不将它们视为重定向。因此,在这两种语法形式中,我们更喜欢双方括号语法。

单方括号和双方括号表达式都可以使用更老式、更类似 Fortran 的语法进行数值比较。例如,它们使用 -le 进行小于或等于比较。这里另一个两者之间的区别出现了。在单方括号表达式中,该运算符两侧的参数必须是简单的整数。使用双方括号时,每个操作数可以是更大的算术表达式,尽管不带空格,除非用引号引起来。例如:

if [[ $OTHERVAL*10 -le $VAL/5 ]] ; then ...

如果你要进行算术表达式和比较,更好的选择是使用双圆括号语法。这样可以使用更熟悉的 C/Java/Python 类型的比较运算符,并且在空格方面更自由:

if (( OTHERVAL * 10 <= VAL / 5 )) ; then ...

风格和可读性:总结

在众多选择中,你会选择哪种 if 语句风格?我们选择最适合考虑的计算的风格。

当它是数学表达式时,我们使用双圆括号。作为一个规则,在 bash 中,双圆括号表示正在进行算术运算。美元符号表示你想要返回表达式的值,否则你只会得到一个成功/失败的结果状态。但是,操作符丰富的 bash 使得可以使用双圆括号语法或 let 内置命令来做类似的事情。由于在双圆括号内不需要在变量前加上 $ 获取它们的值,我们尽量保持一致地省略它们。

对于算术表达式,有些人可能更喜欢在表达式周围使用双圆括号,这与 if 语句保持一致。然而,对于其他人来说,简单的 let 内置命令读起来清晰简单。你可以冒险跳过双圆括号,通过将变量声明为整数,但我们不能推荐这样做。很容易混合和匹配变量,其中一些可能没有被声明为整数。混乱随之而来。将表达式放在双圆括号中(或使用 let)可以保证它仍然是一个算术评估。

对于文本密集的比较,我们使用双方括号,特别是因为这样可以使用正则表达式。

对于条件语句,新的 [[ 语法比 [ 更受欢迎。然而,如果你的条件是算术比较,一个更好的选择是 (( 语法。