Bash Shell 脚本/Shell 算术
Bash 中的算术表达式紧密模仿 C 中的表达式,因此它们与其他源自 C 的语言(如 C++、Java、Perl、JavaScript、C# 和 PHP)中的表达式非常相似。一个主要区别是 Bash 仅支持整数算术(整数),不支持浮点数算术(小数和分数);类似于 3 + 4
的东西意味着您所期望的(7),但类似于 3.4 + 4.5
的东西是语法错误。类似于 13 / 5
的东西是可以的,但执行整数除法,因此计算结果为 2 而不是 2.6。
也许使用算术表达式的最常见方式是在 *算术扩展* 中,其中算术表达式的结果用作命令的参数。算术扩展用 $(( … ))
表示。例如,此命令
echo $(( 3 + 4 * (5 - 1) ))
打印 19
。
使用算术表达式的另一种方法是使用 Unix 程序 "expr",它在 Bash 支持数学之前很流行。[1] 与算术扩展类似,此命令
echo `expr 3 + 4 \* \( 5 - 1 \)`
打印 19
。请注意,使用 "expr" 要求在乘法运算符 "*" 和括号之前使用转义字符 "\"。进一步注意每个运算符符号之间的空格,包括括号。
除了熟悉的表示法 +
(加法)和 -
(减法)之外,算术表达式还支持 *
(乘法)、/
(整数除法,如上所述)、%
(模除,"余数"运算;例如,11 除以 5 是 2 余 1,所以 11 % 5
是 1
)、以及 **
("幂运算",即乘方;例如,24 = 16,所以 2 ** 4
是 16
)。
运算符 +
和 -
除了它们在 "二元"(两个操作数)意义上的 "加法" 和 "减法" 之外,还有在 "一元"(一个操作数)意义上的 "正" 和 "负"。一元 +
基本上没有任何效果;一元 -
反转其操作数的符号。例如,-(3*4)
计算结果为 -12
,-(-(3*4))
计算结果为 12
。
在算术表达式中,可以直接引用 shell 变量,无需使用变量扩展(即无需使用美元符号 $
)。例如,这个
i=2+3
echo $(( 7 * i ))
打印 35
。(请注意,首先计算 i
,产生 5,然后它乘以 7。如果我们写的是 $i
而不是 i
,则只会执行简单的字符串替换;7 * 2+3
等于 14 + 3,即 17——可能不是我们想要的。)
之前使用 "expr" 展示的示例
i=`expr 2 + 3`
echo `expr 7 \* $i`
打印 35
。
Shell 变量也可以在算术表达式中赋值。此表示法类似于常规变量赋值,但 *要*灵活得多。例如,前面的示例可以像这样改写
echo $(( 7 * (i = 2 + 3) ))
除了设置 $i
为 5
而不是 2+3
。请注意,虽然算术扩展看起来有点像命令替换,但它 *不* 在子 shell 中执行;此命令实际上将 $i
设置为 5
,以后的命令可以使用新值。(算术表达式内的括号只是括号的正常数学用法,用于控制运算顺序。)
除了简单的赋值运算符 =
之外,Bash 还支持复合运算符,例如 +=
、-=
、*=
、/=
和 %=
,它们执行一个运算,然后进行赋值。例如,(( i *= 2 + 3 ))
等效于 (( i = i * (2 + 3) ))
。在每种情况下,整个表达式的计算结果都是变量的新值;例如,如果 $i
为 4
,则 (( j = i *= 3 ))
将 $i
和 $j
都设置为 12
。
最后,Bash 支持增量和减量运算符。增量运算符 ++
将变量的值增加 1;如果它 *位于* 变量名称 *之前*(作为 "前增量" 运算符),则表达式的计算结果为变量的 *新* 值,如果它 *位于* 变量名称 *之后*(作为 "后增量" 运算符),则表达式的计算结果为变量的 *旧* 值。例如,如果 $i
为 4
,则 (( j = ++i ))
将 $i
和 $j
都设置为 5
,而 (( j = i++ ))
将 $i
设置为 5
,将 $j
设置为 4
。减量运算符 --
完全相同,只是它将变量的值减少 1。前减量和后减量与前增量和后增量完全类似。
一个命令可以完全由一个算术表达式组成,使用以下语法之一
(( i = 2 + 3 ))
let 'i = 2 + 3'
这两个命令中的任何一个都会将 $i
设置为 5
。两种命令风格的命令都会返回一个退出状态为零("成功" 或 "真")的值,如果表达式计算结果为非零值,则返回退出状态为 1("失败" 或 "假"),如果表达式计算结果为零,则返回退出状态为 1。例如,这个
(( 0 )) || echo zero
(( 1 )) && echo non-zero
将打印这个
zero non-zero
这种反直觉行为的原因是,在 C 中,零表示 "假",非零值(尤其是 1)表示 "真"。Bash 在算术表达式中保持该遗留值,然后在最后将其转换为通常的 Bash 约定。
算术表达式可以包含用逗号 ,
分隔的多个子表达式。最后一个子表达式的结果成为整个表达式的总体值。例如,这个
echo $(( i = 2 , j = 2 + i , i * j ))
将 $i
设置为 2
,将 $j
设置为 4
,并打印 8
。
let
内置命令实际上直接支持多个表达式,而无需逗号;因此,以下三个命令是等效的
(( i = 2 , j = 2 + i , i * j ))
let 'i = 2 , j = 2 + i , i * j'
let 'i = 2' 'j = 2 + i' 'i * j'
算术表达式支持整数比较运算符 <
、>
、<=
(表示 ≤)、>=
(表示 ≥)、==
(表示 =)和 !=
(表示 ≠)。每个表达式对 "真" 计算结果为 1
,对 "假" 计算结果为 0
。
它们还支持布尔运算符 &&
(“与”),如果其任一操作数为零,则计算结果为 0
,否则为 1
;||
(“或”),如果其任一操作数非零,则计算结果为 1
,否则为 0
;以及 !
(“非”),如果其操作数为零,则计算结果为 1
,否则为 0
。除了使用零表示“假”和非零值表示“真”之外,这些运算符与我们在算术表达式之外看到的 &&
、||
和 !
运算符完全相同。与这些运算符一样,这些运算符也是“短路”运算符,如果其第一个参数足以确定结果,则不会计算其第二个参数。例如,(( ( i = 0 ) && ( j = 2 ) ))
不会计算 ( j = 2 )
部分,因此不会将 $j
设置为 2
,因为 &&
的左侧操作数为零(“假”)。
它们还支持条件运算符 b ? e1 : e2
。如果 b
非零,则此运算符计算 e1
并返回其结果;否则,它计算 e2
并返回其结果。
这些运算符可以以复杂的方式组合
(( i = ( ( a > b && c < d + e || f == g + h ) ? j : k ) ))
上面,我们看到了一个 for 循环的样式,它看起来像这样
# print all integers 1 through 20:
for i in {1..20} ; do
echo $i
done
Bash 还支持另一种样式,它模仿 C 和相关语言中的 for 循环,使用 shell 算术
# print all integers 1 through 20:
for (( i = 1 ; i <= 20 ; ++i )) ; do
echo $i
done
此 for 循环使用三个独立的算术表达式,用分号 ;
分隔(而不是逗号 ,
- 这些是完全独立的表达式,而不仅仅是子表达式)。第一个是初始化表达式,在循环开始之前运行。第二个是测试表达式;它在每次潜在的循环迭代之前(包括第一次)进行计算,如果它计算结果为零(“假”),则循环退出。第三个是计数表达式;它在每次循环迭代结束时进行计算。换句话说,这个 for 循环与这个 while 循环完全等效
# print all integers 1 through 20:
(( i = 1 ))
while (( i <= 20 )) ; do
echo $i
(( ++i ))
done
但是,一旦你习惯了语法,它就能更清楚地说明正在发生的事情。
除了常规的算术和布尔运算符之外,Bash 还提供“按位”运算符,这意味着对整数进行操作的运算符,而不是作为整数,而是作为位字符串。如果你还不熟悉这个概念,可以安全地忽略它们。
就像在 C 中一样,按位运算符是 &
(按位“与”)、|
(按位“或”)、^
(按位“异或”)、~
(按位“非”)、<<
(按位左移)和 >>
(按位右移),以及 &=
和 |=
和 ^=
(它们包含赋值,就像 +=
一样)。
整数常量表示为整数字面量。我们已经看到了很多这样的字面量;例如,34
是一个整数字面量,表示数字 34。我们所有的示例都是十进制(十进制)整数字面量,这是默认值;但实际上,字面量可以用 2-64 范围内的任何进制表示,使用表示法 base#value
(其中进制本身用十进制表示)。例如,这个
echo $(( 12 )) # use the default of base ten (decimal) echo $(( 10#12 )) # explicitly specify base ten (decimal) echo $(( 2#1100 )) # base two (binary) echo $(( 8#14 )) # base eight (octal) echo $(( 16#C )) # base sixteen (hexadecimal) echo $(( 8 + 2#100 )) # eight in base ten (decimal), plus four in base two (binary)
将打印 12
六次。(请注意,此表示法仅影响整数字面量的解释方式。无论如何,算术扩展的结果仍然用十进制表示。)
对于 11 到 36 进制,英文字母 A 到 Z 用于表示 10 到 35 的数字值。它不区分大小写。但是,对于 37 到 64 进制,使用小写英文字母表示 10 到 35 的数字值,使用大写字母表示 36 到 61 的数字值,使用符号 @
表示 62 的数字值,使用下划线 _
表示 63 的数字值。例如,64#@A3
表示 256259 (62 × 642 + 36 × 64 + 3).
还有两种特殊表示法:在字面量前加上 0
表示八进制(八进制),在字面量前加上 0x
或 0X
表示十六进制(十六进制)。例如,030
等效于 8#30
,0x6F
等效于 16#6F
。
一个变量可以声明为整数变量 - 也就是说,它的“整数属性”可以“设置” - 使用这种语法
declare -i n
运行上述命令后,对 n
的任何后续赋值将自动导致右侧被解释为算术表达式。例如,这个
declare -i n
n='2 + 3 > 4'
或多或少等效于这个
n=$((2 + 3 > 4))
除了第一个版本的 declare -i n
将继续影响以后的赋值。
在第一个版本中,请注意赋值右侧的引号。如果我们写了 n=2 + 3 > 4
,它将意味着“使用参数 3
运行命令 +
,将环境变量 n
设置为 2
传入,并将标准输出重定向到文件 4
中”;也就是说,设置变量的整数属性不会影响赋值语句的整体解析,而只是控制最终分配给变量的值的解释。
我们可以使用相反的命令“取消设置”变量的整数属性,关闭此行为
declare +i n
declare
内置命令还有许多其他用途:变量可以具有一些其他属性,declare
除了打开和关闭属性之外,还具有其他一些功能。此外,它的几个属性值得注意
- 与
local
和export
一样,参数可以是变量赋值;例如,declare -i n=2+3
设置$n
的整数属性并将其设置为5
。 - 与
local
和export
一样,可以一次指定多个变量(和/或赋值);例如,declare -i m n
设置$m
的整数属性和$n
的整数属性。 - 当在函数内部使用时,
declare
隐式地将变量本地化(除非变量已经是本地的),这也具有在本地取消设置它的效果(除非使用赋值语法)。
如上所述,Bash shell 算术只支持整数算术。但是,外部程序通常可以用来为非整数值获得类似的功能。特别是,常见的 Unix 实用程序 bc
通常用于此目的。以下命令
echo "$(echo '3.4 + 2.2' | bc)"
打印 5.6
。不用说,由于 bc
与 Bash 的集成不像 shell 算术那样紧密,因此它不像 shell 算术那样方便;例如,像这样的东西
# print the powers of two, from 1 to 512:
for (( i = 1 ; i < 1000 ; i *= 2 )) ; do
echo $i
done
为了支持非整数,将变成类似这样的东西
# print the powers of one-half, from 1 to 1/512:
i=1
while [ $( echo "$i > 0.001" | bc ) = 1 ] ; do
echo $i
i=$( echo "scale = scale($i) + 1 ; $i / 2" | bc )
done
部分原因是我们不能再使用算术 for 循环;部分原因是因为引用变量和为变量赋值现在更难了(因为 bc
不了解 shell 的变量,只了解它自己无关的变量);部分原因是 bc
只通过输入和输出与 shell 通信。