跳转到内容

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

expr (已弃用)

[编辑 | 编辑源代码]

使用算术表达式的另一种方法是使用 Unix 程序 "expr",它在 Bash 支持数学之前很流行。[1] 与算术扩展类似,此命令

echo `expr 3 + 4 \* \( 5 - 1 \)`

打印 19。请注意,使用 "expr" 要求在乘法运算符 "*" 和括号之前使用转义字符 "\"。进一步注意每个运算符符号之间的空格,包括括号。

数值运算符

[编辑 | 编辑源代码]

除了熟悉的表示法 +(加法)和 -(减法)之外,算术表达式还支持 *(乘法)、/(整数除法,如上所述)、%(模除,"余数"运算;例如,11 除以 5 是 2 余 1,所以 11 % 51)、以及 **("幂运算",即乘方;例如,24 = 16,所以 2 ** 416)。

运算符 +- 除了它们在 "二元"(两个操作数)意义上的 "加法" 和 "减法" 之外,还有在 "一元"(一个操作数)意义上的 "正" 和 "负"。一元 + 基本上没有任何效果;一元 - 反转其操作数的符号。例如,-(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) ))

除了设置 $i5 而不是 2+3。请注意,虽然算术扩展看起来有点像命令替换,但它 *不* 在子 shell 中执行;此命令实际上将 $i 设置为 5,以后的命令可以使用新值。(算术表达式内的括号只是括号的正常数学用法,用于控制运算顺序。)

除了简单的赋值运算符 = 之外,Bash 还支持复合运算符,例如 +=-=*=/=%=,它们执行一个运算,然后进行赋值。例如,(( i *= 2 + 3 )) 等效于 (( i = i * (2 + 3) ))。在每种情况下,整个表达式的计算结果都是变量的新值;例如,如果 $i4,则 (( j = i *= 3 ))$i$j 都设置为 12

最后,Bash 支持增量和减量运算符。增量运算符 ++ 将变量的值增加 1;如果它 *位于* 变量名称 *之前*(作为 "前增量" 运算符),则表达式的计算结果为变量的 *新* 值,如果它 *位于* 变量名称 *之后*(作为 "后增量" 运算符),则表达式的计算结果为变量的 *旧* 值。例如,如果 $i4,则 (( 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 循环

[编辑 | 编辑源代码]

上面,我们看到了一个 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 表示八进制(八进制),在字面量前加上 0x0X 表示十六进制(十六进制)。例如,030 等效于 8#300x6F 等效于 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 除了打开和关闭属性之外,还具有其他一些功能。此外,它的几个属性值得注意

  • localexport 一样,参数可以是变量赋值;例如,declare -i n=2+3 设置 $n 的整数属性并将其设置为 5
  • localexport 一样,可以一次指定多个变量(和/或赋值);例如,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 通信。

  1. http://www.sal.ksu.edu/faculty/tim/unix_sg/bash/math.html
华夏公益教科书