在 Linux 命令行里做数值计算,最能看出一个人写脚本的老练程度。原本以为“算个数而已”,真动手才发现:echo 3 + 5不会输出 8,而是把字符串原样打出来;echo $((5 / 2))虽然能算,但结果竟然是 2,小数部分被直接丢掉。整数和浮点数在 shell 里几乎是两个世界——前者有 bash 内置的算术展开,速度快写起来也方便;后者则必须借助bc、awk这类外部工具,才能拿到精确的小数结果。这篇博客以“Linux 下进行整数和浮点数运算”为主线,把我实际踩过、处理过、最终沉淀下来的各种操作方式和边界情况都整理出来。无论你是刚接触 Bash 的新手,还是经常写自动化脚本的运维同学,都能在这里找到可以直接抄走的写法,以及一套避坑清单。
1. 整数运算:Bash 自带的算术能力与使用边界
在 Linux 上写脚本,整数运算的第一选择永远是 bash 内置机制,而不是外部命令。原因很直接:内置机制不需要额外创建子进程,在循环里反复计算时性能差距会非常明显。而且 bash 的整数运算语法本身并不复杂,真正重要的是搞清楚几种写法的区别,以及哪些地方容易踩坑。
1.1 算术展开$(( )):最常用的基础写法
$((...))是 bash 提供的最基础整数运算方式,你可以直接把它当成“返回值用”的计算器。最典型的写法是这样:
a=10 b=3 echo $((a + b)) # 13 echo $((a - b)) # 7 echo $((a * b)) # 30 echo $((a / b)) # 3 echo $((a % b)) # 1 echo $((a ** 2)) # 100有两点容易被新人忽略。第一,在$((...))内部,变量名前面的$可以省略,写成$((a + b))完全没问题;但如果你在外面写echo $a + $b,shell 会把它当成两个字符串加一个运算符来处理,不会做算术。第二,bash 的除法是整数除法,7 / 3结果是 2,余数用%单独取,所以需要小数结果时必须另想办法,这一点稍后会展开。
$((...))不只是四则运算,还支持 C 语言风格的一整套运算符。位运算、移位、逻辑判断、三目表达式都可以直接用:
echo $(( (3 + 5) * 2 )) # 16,括号可以改变优先级 echo $(( 8 & 3 )) # 0,按位与 echo $(( 1 << 4 )) # 16,左移 4 位 echo $(( 5 > 3 ? 10 : 20 )) # 10,三目运算 echo $(( 16#FF )) # 255,按十六进制解析 FF echo $(( 2#1010 )) # 10,按二进制解析 1010最后两行里使用的进制#数字写法很实用。比如读取配置时拿到一个0xFF形式的十六进制数字,想参与计算,直接用$((16#FF))就能把它转成十进制数值。二进制、八进制同理。
1.2(( )):把运算变成条件判断
当$((...))外面不带$时,也就是直接写成((...)),它就不再是“取出计算结果”的写法,而是作为一条独立命令存在,根据算术表达式的结果设置退出状态码。简单点理解:运算结果不是 0 时退出码为 0,表示“真”;运算结果为 0 时退出码为 1,表示“假”。
最常见的用法是在if或者while条件里:
x=10 if (( x > 5 )); then echo "x 大于 5" fi这里(( x > 5 ))本身就是一个条件表达式,不需要再写方括号,也不需要用echo $?来判断。配合自增自减和赋值运算,写循环计数器非常方便:
i=0 ((i++)) echo $i # 1 ((i += 2)) echo $i # 3 ((i *= 3)) echo $i # 9$((...))和((...))的区别,本质上是“命令替换”和“命令本身”的区别。前者会替换成一个数值,所以可以放进echo、变量赋值、或者另一个表达式里;后者则主要用来产生退出状态码,控制脚本流程。刚开始写容易混淆,我把它们分别记成“取值写法”和“判断写法”就清晰多了。
1.3 老牌命令expr与let:还能见到,但不必执着
在非常老式的 POSIX 脚本里,你还经常能看到expr和let。expr是个外部命令,除了整数运算还能做字符串匹配、子串提取。它的写法比较刁钻:
expr 3 + 5 # 8 expr 3 \* 5 # 15,乘号必须转义 expr 3 \> 2 # 1,表示逻辑真 expr \( 3 + 5 \) \* 2 # 16,括号也必须转义为什么expr 3 * 5会出错?因为*在 shell 里有通配符展开的语义,会被当前目录下的文件名替换掉;括号、大于号这些字符同样有特殊含义,所以都要用反斜杠或引号包起来。另外expr 3+5也不会计算,它会认为你传入了一个字符串3+5。参数之间必须用空格分隔,不能连在一起。
let是 bash 内置命令,用法如下:
let a=3+5 echo $a let "a = (3 + 5) * 2" echo $a # 16let的表达式里如果包含空格,必须整体用引号扩成一个参数,否则 shell 会把它拆成多个参数导致报错。现实中我很少在脚本里用let,因为((...))写起来更直观,也不会被空格问题困扰。expr也一样,除非是兼容 POSIX sh 的极端需求,否则没必要在纯 bash 环境里自找麻烦。
1.4 整数运算的雷区:溢出、前导零、空变量
bash 的整数是基于 C 语言的long类型实现的。64 位系统上,范围大约是-9223372036854775808到9223372036854775807。一旦超出这个范围,bash 不会给你一个“精度受损”的近似值,而是直接报算术溢出错误。所以处理超大 ID、哈希片段这类数字时,要先想清楚它是否还在 long 范围内。
带前导零的数字也会带来意外。echo $((010))的结果不是 10,而是 8,因为 bash 把010当成八进制。这在我处理日期格式时踩过坑,比如08月、09日这种写法在算术展开里会被判定为非法八进制数。虽然可以用10#08强行按十进制解析,但如果变量本身夹杂着其他字符还是会报错,最好的习惯是在进入计算前先对数据做好清洗。
还有一个隐蔽问题是“未定义变量按 0 处理”。在$((...))里,如果你写了一个不存在的变量名,bash 不会报错,而是默认它是 0。这有时候是好事,比如计数器还没初始化时直接((count++))也能跑;有时候则是坏事,变量名拼错了程序照样运行,结果却完全不对。排查这类问题通常耗时很久,所以我在写脚本时会给关键变量显式初始化。
2. 浮点运算:为什么 Bash 算不了小数,以及如何绕开限制
如果你在 bash 里直接写echo $((5 / 2)),得到的一定是2。这不算 bug,而是 bash 的设计定位:它只需要高效处理整数运算,浮点运算交给外部工具负责。现实中的平均耗时、百分比、利率、精度计算,全都涉及浮点数,因此靠 bash 内建能力是走不通的。
2.1 浮点数比较与赋值的常见报错
除了除法结果被截断,shell 的整数比较操作符也会受限于整数语义。比如下面这种写法会直接报错:
a=3.5 if [ "$a" -gt 2 ]; then echo "yes" fi报错信息通常是integer expression expected,意思是-gt只能比较整数。我见过不少同事跑脚本跑到这里一脸茫然,第一反应是怀疑变量里面有隐藏字符,实际上就是数据类型不支持。遇到浮点比较,原则是“不在方括号里硬顶,直接交给能处理浮点的工具”。
2.2 高精度浮点计算:先学会用bc
bc是 Linux 里最可靠的计算器,支持任意精度。它的基本用法是通过管道把表达式喂给它:
echo "5 / 2" | bc注意,这样输出的结果仍然是2,不是2.5。原因是bc默认的scale为 0,也就是不保留小数位。要让除法结果带小数,必须显式设定scale:
echo "scale=2; 5 / 2" | bc # 2.50 echo "scale=4; 1 / 3" | bc # .3333有人可能会问,既然要算浮点数,为什么bc默认还截断?因为它实际上是任意精度计算器,精度取决于你给它的scale变量,而不是看操作数里有没有小数点。所以写bc表达式时,先把scale写在最前面是个好习惯。
把 shell 变量放进bc表达式也很常见:
a=7 b=2 result=$(echo "scale=3; $a / $b" | bc) echo "$result" # 3.500如果表达式中既有变量又有运算符,推荐用双引号包裹整个表达式。双引号内*不会触发 shell 的通配符展开,所以echo "scale=2; $a * $b" | bc可以放心使用。
bc加-l参数后会加载数学库,支持常见的sqrt、s(正弦)、c(余弦)、a(反正切)、l(自然对数)、e(指数)等函数。比如算平方根:
echo "scale=4; sqrt(2)" | bc -l # 1.4142只写echo "sqrt(2)" | bc在部分版本中会报函数未定义,所以涉及数学函数时务必加上-l。这里区分一下:-l会把默认scale设置成 20,输出很多位小数;如果不需要那么高的精度,再单独指定scale=4即可。
2.3 文本处理场景优先用awk
如果你的数据本来就来自文本文件或管道,比如日志里的响应时间、批量生成的测试数据,那么用awk做浮点运算会比bc更顺手。awk天然按行读数据,自动把字符串解析成浮点数,还能用printf格式化输出。
一个很典型的例子是求和并计算平均值:
awk '{sum += $1; count++} END {printf "%.2f\n", sum / count}' data.log如果文件里某一行读取出来是空的,awk也不会像 bash 那样直接“语法错误”,它会把空字符串当 0 处理,行为更宽容。当需要把 shell 变量带进awk时,用-v传递是最稳妥的方式:
a=7 b=2 awk -v a="$a" -v b="$b" 'BEGIN {printf "%.2f\n", a / b}'这种写法避免了将变量直接拼接进 awk 程序文本,也就规避了注入风险。我写脚本时凡是要把外部输入传给算术工具,统一用-v或环境变量,而不是把字符串直接嵌进去。
2.4 其他可用备选:Python 与通用计算
如果系统里安装了 Python,也可以在内嵌的-c参数里直接做浮点计算:
python3 -c 'print(7 / 2)' python3 -c 'from math import sqrt; print(sqrt(2))'Python 的优势是表达式写起来像普通语言,适合复杂数学和科学计算;缺点是每次调用都会启动一个解释器进程,在循环里频繁执行会很慢。所以我的建议是:简单场景用bc,批量文本场景用awk,只有已经确定要跑较复杂的数学时才把 Python 拉进来。
3. 把整数和浮点运算写进真实脚本:四个典型场景
理论说多了容易飘,这里拆解几个我实际写脚本时反复遇到的场景。你会发现整数和浮点运算往往不是孤立出现的,而是混在一起配合使用。
3.1 用整数纳秒差计算脚本耗时
统计一段代码执行耗时,是脚本开发里非常常见的需求。用date +%s%N可以拿到当前时间的纳秒数,计算结束值和起始值的差,就得到了纳秒级别的耗时:
start=$(date +%s%N) sleep 0.2 end=$(date +%s%N) echo "总耗时 $(( (end - start) / 1000000 )) ms"这里end - start是巨大的纳秒整数,除以1000000就得到毫秒。用 bash 整数运算处理是因为这是纯整数的差,效率高,也不需要任何外部进程。如果你的系统date不支持%N,只能用秒级时间戳,那脚本精度就只能到秒,这一点要先确认。
如果还想让结果精确到小数点后两位,可以把纳秒差交给bc:
elapsed_ns=$((end - start)) echo "scale=2; $elapsed_ns / 1000000000" | bc这个组合很有意思:中间计算依然用整数,最后一步“除法并保留小数”再用bc完成。这样避免在脚本核心循环里反复启动外部进程,又能保证输出格式漂亮。
3.2 计算百分比时先判断分母
成功率、命中率、占用率这类百分比计算,是最容易“看起来没问题但结果错了”的场景。假设脚本读到了两个整数ok和total,想输出成功率:
ok=13 total=20 if (( total == 0 )); then percent=0 else percent=$(echo "scale=2; $ok * 100 / $total" | bc) fi echo "成功率 ${percent}%"这里有两个关键点。第一,total为 0 时必须提前拦住,否则bc会报Divide by zero,脚本很可能直接中断。第二,scale=2只是控制小数位数,并不会自动四舍五入,它更像是“截断到两位小数”,这也是很多人踩过的坑。
3.3 从日志列里算平均值,连文本清洗一起做
有一次我要统计一批接口响应时间,数据文件里每行一个毫秒数,中间还有空行和异常标记。直接交给awk一次就能搞定:
awk '$1 ~ /^[0-9.]+$/ {sum += $1; count++} END {printf "samples=%d average=%.2f\n", count, count ? sum / count : 0}' timing.log这里$1 ~ /^[0-9.]+$/是一个正则过滤,只累加纯数字行;count ? sum / count : 0则保护了空文件场景,避免出现除零导致的inf。这种“过滤、求和、格式化”三合一的能力,是awk在浮点计算里最舒服的地方。
3.4 循环里生成随机数并算比例
bash 自带的RANDOM变量只能生成 0 到 32767 的整数。想要模拟一个 0 到 100 之间的随机百分比,常规写法是:
a=$((RANDOM % 101)) b=$((RANDOM % 3 + 1)) awk -v a="$a" -v b="$b" 'BEGIN {printf "%.3f\n", a / b}'第一部分用 bash 整数运算生成随机整数并控制范围,第二部分把变量传给awk做浮点除法。这种混合模式在写测试脚本、抽样脚本时非常常见,也再一次说明:整数计算负责流程控制,浮点计算负责最终数值,两者分工明确。
4. 高频踩坑与排查经验实录
这一节我把这些年实际操作中积累的报错、排查和规避经验集中清理一遍。每一条都是我在真实脚本里碰到过、查过很久才定位的问题,值得收藏。
4.1 明明打印出来是数字,为什么一计算就报错
现象是变量看起来是数值,但放进$((...))或[ ]比较时立刻报错。最常见的原因是变量里面带有小数点、空格或隐藏字符。比如某个命令返回了"1.5 KB",打印出来看着挺正常,实际字符串里既有小数又有单位。
排查方法很简单,先做可视化输出:
printf '[%s]\n' "$n"如果在方括号里能看到多余空格或字母,就说明不是纯数字。解决办法是先清洗,再计算。如果需要保留小数,用awk做更稳妥:
n="1.5 KB" clean=$(awk -v raw="$n" 'BEGIN {gsub(/[^-0-9.]/, "", raw); print raw}') echo "$clean" # 1.5给外部输入做白名单式过滤,只保留数字、小数点和负号,能避免大量隐性问题。
4.2 浮点比较的正确打开方式
既然[ "$a" -gt 2 ]不支持浮点,那浮点比较怎么写?最常用的是借助bc:
a=3.5 if (( $(echo "$a > 2" | bc) )); then echo "yes" fi当条件成立时,bc输出1,((1))的退出状态码为 0,进入 if 分支;不成立时输出0,退出状态码为 1,跳过分支。这个技巧用起来很方便,但要注意注入风险:如果$a来自不受信任的输入,直接拼进bc表达式可能会被拼接成其他命令。稳妥做法是先用正则或awk验证它确实是一个合法的数值字符串。
4.3bc的scale不是四舍五入,而是截断
很多人写echo "scale=2; 20 / 3" | bc期望得到6.67,结果却是6.66,然后怀疑人生。原因在于默认scale行为是对多余位数直接截断,不是四舍五入。如果非要“四舍五入到两位”,更可靠的做法是交给printf格式化:
orig=$(echo "scale=2; 20 / 3" | bc) echo "$orig" # 6.66 rounded=$(printf "%.2f\n" "$(echo "20 / 3" | bc -l)") echo "$rounded" # 6.67这里bc -l先用足够精度算出小数,再由printf的%.2f负责真正舍入。别小看printf,bash 内置的printf本身就是可以格式化浮点数的,只是需要提供一个精度足够的数字给它。
4.4bc进制转换:顺序错了直接乱套
用bc做十六进制和二进制转换时,ibase和obase的顺序是个经典坑。正确写法是:
echo "ibase=16; FF" | bc # 255 echo "obase=2; ibase=16; FF" | bc # 11111111很多人会写成ibase=16; obase=2; FF,结果得到一堆看不懂的数字。原因很微妙:ibase=16一旦生效,后面的obase=2也会被当作十六进制数来解释,于是2不再是二进制,而是十六进制的2,等于十进制的2,进制设置自然就乱了。所以我的习惯是先把obase写在前面,再写ibase,这样可以避免前面说的自我污染。
4.5 大数精度:awk 与 bc 的取舍
awk和 Python 走的是浮点路线,内部用双精度表示,精确表示的范围大概到 2 的 53 次方左右。超过这个范围,浮点表示会丢失精度,只是大多数脚本场景看不出来。如果计算对象是超长数字,比如设备序列号、大额订单号、精确到分的超大金额,建议直接用bc:
echo "1.234567890123456789 * 2.0" | bc -lbc任意精度意味着不会因为这些数字超出浮点数范围而输出近似值。判断标准很简单:看你对精度要求有多高,以及数据会不会超过普通人使用的“日常量级”。
5. 方案选择速查表与我的个人习惯
不同场景对应不同工具,这里总结一份我写脚本时经常参考的对照表。它不是“标准答案”,但能帮你少走不少弯路。
| 需求 | 推荐命令 | 说明 |
|---|---|---|
| 纯整数赋值、循环、条件判断 | (( ))/$(( )) | bash 内置,最快最直接 |
| 整数位运算、进制写法 | $(( )) | 支持16#FF、2#1010 |
| 高精度浮点、复杂数学函数 | bc -l配合scale | 任意精度,支持sqrt、sin等 |
| 文本列求和、平均、统计 | awk | 避免把数据搬来搬去,一次处理 |
| 浮点数四舍五入显示 | printf | 与bc联用,负责最终格式 |
| 老式 POSIX 脚本兼容 | expr | 注意转义,能不用就不用 |
| 较复杂的数学逻辑 | python3 -c | 适合一次性的科学计算 |
5.1 我常用的封装函数
写脚本多了之后,我习惯给常用功能做一个小封装,避免每个脚本里都重复写一遍。比如下面这个简单函数,调用calc就能直接算带小数的表达式:
calc() { printf "%.4f\n" "$(echo "$1" | bc -l)" }使用方式:
calc "7 / 2" calc "scale=4; 10 / 3"这个函数看起来很投机取巧,但它把bc调用和printf格式化串起来之后,日常脚本里的浮点需求基本都能覆盖。你还可以根据自己的习惯调整输出位数,比如改成%.2f。
5.2 我在实际脚本里的一个习惯
最后分享一个我的私人经验:写算术逻辑前,先确定输入数据是不是“干净数字”。无论是命令输出、配置文件还是用户参数,只要没经过校验就直接参与计算,迟早会在某个犄角旮旯碰到不带单位但带小数点的变量,或者带前导零的日期数字。我现在的习惯是先写一个小验证函数,把不符合数字格式的输入直接拦截,或者先用awk清洗再进入计算流程。
这一点看起来琐碎,却是脚本从“能跑”走向“稳跑”的分水岭。数值类型搞清楚了,脚本计算的可预测性会大幅提升。如果你打算把这套知识点真正用起来,强烈建议先在本地写几个包含整数、浮点、进制转换的小脚本,把本文提到的报错场景亲手触发一遍,记忆会更深刻。