☰
Shell算术运算全攻略:从$(( ))到bc/awk,避开整数陷阱
2026/10/5 11:40:37 网站建设 项目流程

很多人第一次在Shell里做数学计算时,都会遇到同一个困惑:明明在别的语言里"a + b"写得好好的,到了Shell里要么输出个"2+3"的字符串,要么报command not found,要么算出来的结果莫名其妙少个位数。这不是你写错了,而是Shell本身对"运算"的理解和普通编程语言完全不一样——在Shell的世界里,一切变量本质都是字符串,加号、乘号对Shell来说只是普通字符,你需要用专门语法告诉它"这里要做算术运算"。

这里说的Shell,指的是Linux/Unix环境下的命令解释器(bash、sh、zsh),不是EFI Shell,也不是嵌入式领域那种面向单片机的Letter Shell。这篇内容围绕Shell数学计算(算术运算,加减乘除运算)做一次完整梳理,把$(( ))、expr、let、bc、awk这几条路线讲透,并给出实战案例和面试中常出现的坑。适合刚入门Shell脚本的初学者,也适合写过一段时间但总在算术上翻车的同学,看完可以直接照着抄,也可以当工具手册收藏。

1. 先理清Shell算数的底层逻辑:字符串、整数、浮点三个世界

1.1 为什么a+b会输出一个字符串

很多人第一次写Shell算术,都是这样的:

a=2 b=3 echo $a + $b

输出结果是2 + 3,而不是5。原因很简单:Shell变量没有类型,a=2存进去的是字符"2",$a + $b在Shell看来就是三个独立的单词,echo 把这三个单词原封不动打印出来而已。你从来没有告诉Shell"请把2和3加起来",Shell自然也不会主动去做数学题。

所以要记住第一个结论:Shell里的运算符只是普通字符,只有放进特定的"算术上下文"里,它们才会被当作真正的数学运算符来解析。这就是$(( ))、(( ))、let、expr存在的意义。

1.2 整数与浮点是两个战场

Shell内建的算术运算,只支持整数。这一点特别坑人:很多从Python、JavaScript转过来的人,第一次写echo $((5 / 2))发现输出2,当场懵了。小数呢?被截断了,不是四舍五入,是直接丢掉小数部分。

这不是bug,是设计如此。Bash在整数计算上用的是64位有符号整型,效率高、实现简单,但代价就是不支持分数、小数。一旦你的脚本里出现百分比、平均值、费率计算这类需求,就必须引入外部工具,最常用的两个是bc和awk。

所以做Shell算术之前,先问自己一个问题:我要算的是整数还是浮点?这决定了你选哪条技术路线。整数走内建,浮点走外挂,两条路混着用也没问题,但心里得先有这个分类。

1.3 bash、sh、zsh之间的差异不能忽略

同样是算术,不同Shell的支持程度不一样。expr是外部命令,任何Shell里都能用;$(( ))是POSIX标准语法,bash、sh、dash、zsh都支持;但let、declare -i、(( ))里的一些扩展运算符(比如**幂运算)是bash专属,在纯POSIX的dash里可能就不好使了。

方案整数运算浮点运算POSIX兼容是否依赖外部命令
$(( ))支持不支持支持否
(( ))支持不支持基本兼容(bash扩展)否
let支持不支持不支持(bash内建)否
declare -i支持不支持不支持(bash内建)否
expr支持不支持支持是(/usr/bin/expr)
bc支持支持依赖管道是
awk支持支持依赖管道是

我个人的习惯是:脚本开头#!/bin/bash就放心用bash特性,但如果要写那种装在任何系统上都能跑的POSIX脚本,老老实实用$(( ))最保险。

2. 主推方案 $(( )):从加减乘除到赋值运算一次讲透

2.1 基础语法与运算符合集

$(( ))是Bash和POSIX Shell都支持的算术扩展语法,用法非常简单:

echo $(( 2 + 3 )) # 输出5 echo $(( 10 - 4 )) # 输出6 echo $(( 3 * 4 )) # 输出12 echo $(( 7 / 2 )) # 输出3,整数除法 echo $(( 7 % 2 )) # 输出1,取余数 echo $(( 2 ** 10 )) # 输出1024,幂运算(bash支持)

核心运算符就是加减乘除和取模,你在算术课上学的优先级在这里同样有效:先乘除后加减,括号可以改变顺序。

echo $(( (1 + 2) * 3 )) # 输出9 echo $(( 1 + 2 * 3 )) # 输出7

需要注意,$(( ))里面空格是可选的,$((2+3))和$(( 2 + 3 ))结果一样,这点跟后面的expr完全不同。

赋值给变量是日常最常用的姿势:

total=$(( 5 + 5 )) echo $total # 输出10

2.2 变量参与计算:加不加$的讲究

变量参与算术,可以用$也可以不用:

a=10 b=3 echo $(( a + b )) # 输出13 echo $(( $a + $b )) # 同样输出13

我推荐不加$的写法。原因有两个:第一,$(( ))内部本身就是一个算术求值环境,变量名会被自动解析成值,没必要多写符号;第二,少了$,你的表达式更接近普通编程语言,阅读起来舒服。这种写法在C语言风格for循环里特别常见,比如for ((i=0; i<10; i++)),从没人写$i。

$(( ))还有一个隐藏特性:如果变量的值本身是一个算术表达式,参与计算时会被"二次求值"。

a="2+3" echo $(( a * 2 )) # 输出10,等价于 (2+3)*2

这个特性偶尔能派上用场,但也容易让脚本变得难懂,日常不建议刻意用。

2.3 自增自减与复合赋值

循环里免不了要写i = i + 1这种累加,Shell提供了几种简写:

i=0 (( i++ )) # i变为1,作为语句使用 (( i += 5 )) # i变为6,复合赋值 (( i = i * 2 )) # i变为12

$(( ))和(( ))的区别要特别讲清楚,这是新手最容易混的地方:

  • $(( ))会输出计算结果,适合用来取值赋值。echo $(( 1 + 1 ))输出2。
  • (( ))不会输出任何内容,它只做计算并返回一个退出状态码,结果非0则状态码为0(真)。
(( 3 > 2 )) echo $? # 输出0,表示条件为真

所以(( ))天生适合做if判断:

a=10 if (( a > 5 )); then echo "a大于5" fi

$((a++))和((a++))的值也有区别。echo $((i++))是先输出i的当前值,再把i加1;echo $((++i))是先加1再输出。如果你只是想让变量自增、不需要输出,直接用((i++))最干净。

2.4 位运算、逻辑运算与三元表达式

除了加减乘除,$(( ))还支持位运算和逻辑运算,写权限判断、掩码计算时很实用:

echo $(( 5 & 3 )) # 按位与,输出1 echo $(( 5 | 3 )) # 按位或,输出7 echo $(( 5 ^ 3 )) # 按位异或,输出6 echo $(( 1 << 4 )) # 左移4位,输出16 echo $(( 8 >> 2 )) # 右移2位,输出2

逻辑运算和三元表达式也支持:

echo $(( 3 > 2 && 4 > 3 )) # 输出1,逻辑与 echo $(( 3 > 2 ? 100 : 200 )) # 三元运算,输出100

2.5 进制转换:一个容易被忽视的隐藏功能

$(( ))可以处理不同进制的数值,语法是基数#数值:

echo $(( 2#1010 )) # 二进制1010,输出10 echo $(( 16#FF )) # 十六进制FF,输出255 echo $(( 8#17 )) # 八进制17,输出15

这个功能在处理配置文件、内存地址、权限掩码这类场景时特别好用。反过来的转换(十进制转十六进制)可以用printf:

printf "%x\n" 255 # 输出ff printf "%X\n" 255 # 输出FF

提示:$((16#FF))这种方式是bash的算术扩展支持的,POSIX严格模式下不一定都好使,跨平台脚本慎用。

3. expr、let、declare -i:三种老牌写法的脾气与选型

3.1 expr的三个反直觉约束

expr是最古老的Shell算术工具之一,很多老教程和面试题里都会出现。但它有三个特别反直觉的约束,你没用过的话第一次几乎必踩坑。

第一个约束:操作数和运算符之间必须有空格。

expr 2 + 3 # 输出5 expr 2+3 # 输出2+3,原样打印

第二个约束:乘号必须转义。因为*在Shell里是通配符,expr 2 * 3会把*展开成当前目录的文件列表,所以必须写成\*:

expr 2 \* 3 # 输出6

第三个约束:括号也要转义:

expr \( 2 + 3 \) \* 4 # 输出20

另外,expr的计算结果是通过标准输出返回的,想拿结果给变量赋值,必须用命令替换:

result=$(expr 2 + 3) echo $result # 输出5

expr还有个隐藏的性能短板:它是外部命令,每次执行都要fork一个进程。在万次循环里用expr做累加,速度比内建$(( ))慢几十倍甚至上百倍。所以我现在的脚本里基本不碰expr,除非是在写极简POSIX脚本且不想依赖bash特性。

3.2 let:bash内建但存在感很低

let是bash的内建命令,专门用来做整数赋值运算:

let a=2+3 echo $a # 输出5

let和$(( ))的最大区别是:let不输出计算结果,只把结果赋给变量。它支持+=、-=、++、--这类复合运算:

let a=5 let a+=3 # a变成8 let a++ # a变成9

一个容易踩的坑:let后面的表达式如果带空格,会被Shell拆成多个参数,必须用引号包起来:

let "a = 2 + 3" # 正确 let a = 2 + 3 # 报错或行为异常

let能干的事,$(( ))和(( ))都能干,所以它的定位比较尴尬。我在生产脚本里几乎不用它,唯一的例外是写那种逗号风格的连续赋值,比如let "i=0, j=10"。

3.3 declare -i:让变量"变成"整数

declare -i可以声明一个整数类型的变量,之后对这个变量的赋值会自动当作算术表达式求值:

declare -i num=10 num=num+5 echo $num # 输出15,而不是"num+5"

注意这里赋值的时候不需要写$(( )),系统会自动做算术求值。如果你给整数变量赋一个非数字字符串,它会变成0:

declare -i n n="hello" echo $n # 输出0

declare -i在函数里可以作为local -i使用,局部整数变量的语义很清楚。不过说实话,我现在用到它的场景很少,因为它的隐式求值机制对不熟悉的同事来说不够直观,反而降低了脚本的可读性。

3.4 选型建议:什么时候用谁

场景推荐方案理由
通用脚本、跨Shell运行$(( ))POSIX标准,语法干净
循环累加、计数器(( i++ ))或i=$((i+1))内建命令,性能好
if条件判断(( 条件 ))退出状态码语义清晰
老系统兼容脚本expr外部命令,到处都有
需要浮点运算bc或awk内建不支持小数
处理文本列数据awk天然支持逐行处理和浮点

一句话总结:能用$(( ))的地方就别用expr,能加bc的地方就别自己折腾整数逼近。

4. 除法的整数陷阱:引入bc与awk解决浮点运算

4.1 为什么5/2等于2

这是Shell算术里最经典的"意外"。在bash里:

echo $(( 5 / 2 )) # 输出2 echo $(( 5 % 2 )) # 输出1

整数除法的规则是向零截断,也就是直接丢掉小数部分,不做四舍五入。负数的情况也遵循这个规则:

echo $(( -5 / 2 )) # 输出-2,不是-3 echo $(( -5 % 2 )) # 输出-1,余数符号跟随被除数

如果你确实只需要整数商,这个行为完全没问题。但如果你算的是平均值、百分比、速率,就得分外小心。比如计算"3个任务平均耗时",$(( total / 3 ))把小数全吞了,结果误差累积起来很离谱。

判断奇偶是取模运算最经典的用法:

n=7 if (( n % 2 == 0 )); then echo "偶数" else echo "奇数" fi

4.2 bc:最常用的浮点计算工具

bc是一个任意精度的计算器语言,通过管道把表达式喂给它就能得到结果:

echo "5 / 2" | bc # 输出2,因为scale默认是0 echo "scale=2; 5 / 2" | bc # 输出2.50

关键是scale这个参数,它控制小数点后的保留位数。不设置的话,整除和小数除法都只出整数部分。设置了scale,除法就会保留指定位数的小数。

echo "scale=4; 10 / 3" | bc # 输出3.3333

bc -l会加载数学库,同时把scale默认设为20,适合做科学计算风格的运算:

echo "scale=2; sqrt(2)" | bc -l # 输出1.41 echo "scale=2; 3.14 * 2" | bc -l # 输出6.28

bc支持变量和多步计算,管道里写多行也没问题:

echo "x=5; y=2; x / y" | bc

我实际用得最多的场景是算百分比、算等待时间、算平均值。比如脚本里要根据文件总数动态计算每批处理的数量:

total=100 batch=7 per_batch=$(echo "scale=2; $total / $batch" | bc) echo "每批处理: $per_batch"

提示:bc的scale只对除法、取模等部分运算生效,乘法的小数位规则更复杂一点,日常计算建议显式写scale,不要依赖默认行为。

4.3 awk:另一个顺手的选择

awk本身就是一个完整的文本处理+计算语言,浮点运算对它来说是家常便饭:

awk 'BEGIN { print 5 / 2 }' # 输出2.5 awk 'BEGIN { printf "%.2f\n", 10 / 3 }' # 输出3.33

要在awk里用Shell变量,通过-v传进去:

a=5 b=2 awk -v num1=$a -v num2=$b 'BEGIN { printf "%.2f\n", num1 / num2 }'

awk的另一大优势是天生面向"按行处理数据"。比如一个文件里每行一个数字,我想算平均值:

awk '{ sum += $1; count++ } END { printf "平均: %.2f\n", sum / count }' numbers.txt

这种场景如果用纯Shell写,先while循环读文件、再做$(( ))累加、最后再用bc算除法,中间要写好几行;awk一行搞定,还自动支持浮点。所以我的建议是:单次数值计算用bc,批量处理文本数据里的计算用awk。

4.4 实战:百分比、平均值、限速值

结合上面讲的,给几个可以直接抄的计算片段。

计算百分比并保留两位小数:

part=37 total=200 awk -v p=$part -v t=$total 'BEGIN { printf "占比: %.2f%%\n", p * 100 / t }'

计算一组数字的平均值(数据在文件里):

awk '{ sum += $1 } END { print "平均值:", sum / NR }' data.txt

给while循环的sleep算一个动态间隔(比如1分钟分N次执行):

count=6 interval=$(echo "scale=2; 60 / $count" | bc) while (( i < count )); do echo "第 $i 次执行" sleep "$interval" (( i++ )) done

5. 实战拆解:循环计数器、累加求和与脚本计时

5.1 for循环里的两种写法

Shell数学计算在循环里用得最多。最常见的C风格for循环,本质就是一个算术表达式:

for (( i = 1; i <= 10; i++ )); do echo $i done

另一个常见写法是用seq:

for i in $(seq 1 10); do echo $i done

两者区别在于:seq是外部命令,每次执行都要起进程,数值大时性能差;C风格for循环是bash内建的算术求值,速度快很多。我写循环默认用C风格for。

5.2 累加求和的标准姿势

求和1到100,是Shell脚本入门级别的经典题,也是面试爱出的基础题:

sum=0 for (( i = 1; i <= 100; i++ )); do sum=$(( sum + i )) done echo "1到100的和: $sum" # 输出5050

用while循环写也一样:

sum=0 i=1 while (( i <= 100 )); do sum=$(( sum + i )) (( i++ )) done echo $sum

这里的sum=$(( sum + i ))和(( sum += i ))完全等价,看个人偏好。我习惯用sum=$(( sum + i )),因为一眼就能看出"把sum和i相加后重新赋给sum",语义更直白。

5.3 用时间戳计算脚本耗时

算耗时是运维脚本里的高频需求,核心就是"结束时间减开始时间":

start=$(date +%s) # 中间是你要计时的逻辑,比如sleep 3 sleep 3 end=$(date +%s) elapsed=$(( end - start )) echo "耗时: ${elapsed}秒"

如果任务很快,秒级不够用,就用纳秒时间戳:

start=$(date +%s%N) # 执行某个命令 end=$(date +%s%N) elapsed_ms=$(( (end - start) / 1000000 )) echo "耗时: ${elapsed_ms}毫秒"

这里date +%s%N返回的是从1970年1月1日到现在的秒数+纳秒数,是一个很大的整数,做减法属于纯整数运算,正好是$(( ))的强项。

5.4 批量重命名中的数学运算

批量重命名文件时,经常需要按序号重新编号。这里有个组合技巧:用算术生成序号,用printf补零:

n=0 for f in *.jpg; do n=$(( n + 1 )) mv "$f" "photo_$(printf "%03d" $n).jpg" done

运行后,a.jpg、b.jpg、c.jpg会被重命名为photo_001.jpg、photo_002.jpg、photo_003.jpg。

有个细节值得注意:补零用printf的%03d,不要试图用算术生成001这样的数字。因为Shell里$(( 001 + 1 ))会把001当作八进制处理,一旦数字里有8或9就会直接报错。这就是下一章要讲的前导零陷阱。

6. 踩坑与面试:Shell算术的高频雷区和经典考法

6.1 空变量与未定义变量

在bash里,未定义或空变量参与算术,会被当作0处理:

unset x echo $(( x + 1 )) # 输出1

这个默认行为在大多数情况下很友善,但也容易掩盖bug。比如你从配置文件里读了个数字,结果字段拼错了读成了空值,脚本不会报错,只是计算结果是错的,排查起来很费劲。

如果你希望脚本更严格,可以用set -u打开"未定义变量即报错"的模式。但要注意,set -u对算术上下文里的变量引用也会生效,稳妥的做法是显式给默认值:

echo $(( ${x:-0} + 1 )) # x未定义时用0

这个:-写法是参数扩展语法,意思是"如果x未定义或为空,就使用0"。在算术上下文中这么写,既保证了安全,又避免了报错,是我写生产脚本时的固定习惯。

6.2 前导零的八进制地狱

先说结论:在Shell算术里,以0开头的数字会被当成八进制。

echo $(( 010 )) # 输出8,不是10 echo $(( 08 + 1 )) # 报错:value too great for base (error token is "08")

08、09之所以报错,是因为八进制里根本没有8和9这两个数字。这个坑最容易出现在处理日期时,比如从date +%m或date +%d拿到月份、日期,系统会默认补零输出07、08、09:

month=$(date +%m) # 可能是08 echo $(( month + 1 )) # 报错

解决办法是用10#数字强制按十进制解析:

month=08 echo $(( 10#$month + 1 )) # 输出9

如果变量可能为空,再叠加:-默认值:

echo $(( 10#${month:-0} + 1 ))

这个坑在面试里也经常被拿来考人,属于"看起来很简单,一写就翻车"的典型。

6.3 除以0、整数溢出

除以0在bash里会直接报错并终止算术求值:

echo $(( 1 / 0 )) # bash: division by 0 (error token is "0")

写脚本时如果除数是变量,一定要先判断它是否为0:

divisor=0 if (( divisor == 0 )); then echo "除数不能为0" else echo $(( 100 / divisor )) fi

另外一个进阶知识点是整数溢出。Bash的整数运算基于64位有符号整型,超出范围会回绕:

echo $(( 9223372036854775807 + 1 )) # 输出-9223372036854775808

正常脚本几乎碰不到这么大的数,但写那种长跑监控脚本、累加计数器时,心里有个数总没坏处。

6.4 高频面试题与易错点

下面是Shell算术相关的经典面试题,我把考点拆开讲:

第一个:expr 2+3输出什么?为什么?

输出2+3。因为expr要求操作数和运算符之间必须有空格,2+3被当成一个整体字符串,expr拿到后无法识别运算符,只能原样输出。

第二个:$(( ))和$( )有什么区别?

$(( ))是算术扩展,做数学计算并返回数值;$( )是命令替换,执行里面的命令并返回命令的标准输出。echo $((1+1))输出2,echo $(echo 1+1)输出1+1。这两个长得像,含义完全不同,是面试官最爱考的混淆点。

第三个:如何计算1/3并保留两位小数?

两种标准答案:

echo "scale=2; 1 / 3" | bc # 输出0.33 awk 'BEGIN { printf "%.2f\n", 1 / 3 }' # 输出0.33

第四个:let "i++"、((i++))、i=$((i+1))三者的区别?

三个都能让i加1。区别在于:let "i++"是bash内建命令,不推荐但能用;((i++))是最简洁的自增写法,适合单独作为语句;i=$((i+1))语义最直白,适合赋值场景。面试答出"都会自增,((i++))最常用、$(( ))可以取返回值"就够分了。

第五个:判断一个数是否为偶数的Shell写法?

if (( n % 2 == 0 )); then echo "偶数" fi

第六个:expr和$(( ))哪个快?为什么?

$(( ))快。因为expr是外部命令,每次调用都要fork一个子进程;$(( ))是Shell内建求值,不额外起进程。在循环里做大量计算时,性能差距会非常明显。

我在实际使用中的体会是:Shell算术的上手门槛不在语法,而在思维转换。你只要时刻记住"变量是字符串""整数浮点要分清""前导零是八进制"这三点,90%的坑都可以绕开。剩下的就是多写多练,把它变成肌肉记忆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询