2026/10/3 11:26:33

Linux Shell整数与浮点运算避坑指南:从除法截断到bc/awk选型

Linux Shell整数与浮点运算避坑指南:从除法截断到bc/awk选型 我第一次正经研究 Linux 下的整数运算和浮点数运算是因为磁盘巡检脚本彻底翻车。逻辑明明是 used 除以 total、再乘 100 得到使用率百分比结果跑出来全是 0——瞟一眼代码我以为命令拼错了后来才意识到shell 的算术展开根本不处理浮点数两个整数相除小数部分直接截断used 比 total 小的时候结果就是 0。这个坑看着小但我带过的人里能马上答出“shell 里 1/3 等于多少”的真的不多很多 Linux 面试题也爱拿这类问题做筛选。后面我花了不少时间把这套东西彻底理了一遍整数运算有哪些写法、各有什么边界浮点数该交给哪些工具、每个工具适合什么场景脚本里还有哪些容易翻车的细节。这篇文章就把这些内容一次讲清楚最后附上两个可以直接抄的脚本案例。无论你是刚入门 Linux 的运维还是写自动化脚本的开发者应该都能从这里找到对应的解决路径。1. 整数运算五件套算术展开、let、expr 与条件式1.1 先记住结论$(( ... )) 是最稳的基础写法$(( ... )) 是 POSIX sh 规范里就有的算术展开bash、dash、zsh、busybox 的 ash 都能跑。跨 shell 兼容性是它最大的价值你在系统脚本里随便用都不会惹麻烦。used50 total200 percent$((used * 100 / total)) echo $percent # 25写法上有几个点值得注意。第一算术上下文里变量名前面的 $ 可以省略$((used * 100 / total)) 与 $(( $used * 100 / $total )) 等价但不要两个都写。第二* 和 ( ) 在算术展开里不需要转义因为双括号已经把计算区域隔离出来你要是用 expr 写乘法就得写成2 \* 3这是两个时代的差异。第三除法是截断取整而不是四舍五入这个语义太容易出错先用乘法再除往往能保住精度后面第 3 章会专门讲。基础运算符里除了 - * / %bash 还支持幂运算 **dash 不支持跨脚本注意、位运算 | ^ ~ 、三目运算符 ?: 以及 C 风格的一元自增自减。我举个位运算的例子mode6 if (( mode 0x2 )); then echo write bit is set fi同一个小节放运算符速查表运算符说明示例结果 - * /加减乘除整除截断$((7 / 2))3%取模$((7 % 3))1**幂运算bash$((2 ** 10))1024 左移右移$((1 4))16^ ~按位与、或、异或、取反$((6 3))?:三目运算$(( 2 1 ? 10 : 20 ))10 --自增自减((i))—加减乘除、取模、比较这些就是日常主力其他运算符平时用到频率不高但知道边界在哪里比背下来更重要。1.2 let 与 (( ... ))既能算又能当条件判断let 是 shell 内置命令直接let a12就能计算(( ... )) 则是在 let 基础上的语法糖同样不依赖外部进程。它们的优势不只是快而是计算结果可以当返回值用——(( expr )) 的退出状态是 0 还是 1由表达式是否为 0 决定。所以常见写法是a5 (( a 3 )) if (( a 6 )); then echo a is now $a # 输出 a is now 8 fi这种写法比if [ $a -gt 6 ]更直接也避免了字符串和整数之间来回转换。在 for 循环里也很典型for (( i 0; i 10; i )); do printf %s $i donelet 单独用有个小坑let 后面的赋值不能带空格let a 1会解析错误必须写成let a1。几乎所有 Linux 发行版内置的 bash 都支持 let但 POSIX 标准里并不包含它跨 sh 脚本建议还是统一用 $(( ... ))。我实际测试过 expr 和内部算术的差距在循环里跑一万次简单表达式expr 的耗时是 $(( ... )) 的几十倍。原因也简单expr 是外部命令每调用一次都要 fork 一个进程出来同一份工作内部算术在 shell 进程内就完成了。这不是精度问题是性能问题写循环计算时尤其明显。1.3 expr 和 $[ ... ]老用法认识一下就行expr 是外部命令早期没有 $(( )) 时全靠它。它要求每个操作数之间用空格隔开乘法要转义返回码的语义也跟直觉不一样写起来相当啰嗦expr 1 2 expr 2 \* 3expr 在现在的脚本里几乎可以被完全替代面试题里偶尔还会出现。如果你在维护老项目时看到它知道它在干什么就够了新代码不要主动引入。$[ ... ] 是更早的遗留语法功能上类似 $(( ))但不再被推荐见到也可以放心替换成 $(( ))。1.4 整数运算的边界比语法更重要整数计算在脚本里的用途主要是数组下标、循环计数、权限位判断、百分比换算。说句实在话这类场景基本不会碰到的边界只有两条溢出和非法值。shell 算术内部用有符号 64 位整数数值超出 2^63-1 时bash 会报错或回绕取决于版本和上下文。普通脚本碰不到这么大数但如果你在拼 SQL 或者做 ID 累加就得留个心眼这种情况应该把自己的计算逻辑搬到 boby 工具而不是硬扛。非法值更隐蔽。变量未定义或为空时算术展开默认按 0 处理a; echo $((a 1))会输出 1乍一看没什么问题但如果变量名拼写错误这个特性会直接把错误吞掉脚本继续跑出错误结果。我的习惯是脚本开头加set -u让未定义变量直接报错宁可在开发期炸出来也不要上线后算出一个谁都没注意的歪数。2. 浮点数的正确打开方式bc、awk、python3 怎么选2.1 先理解为什么 shell 自己不处理浮点数很多初学者容易把期望放错位置在 shell 里写echo $((1 / 3))期待输出 0.333结果被无情截断成 0。这不是你写错了是 shell 的设计哲学本就不是做计算器。早期 shell 的算术机制是为了数组下标这种场景设计的整数刚好够用浮点计算属于外部工具的职责POSIX 规范也一直保持这个边界。所以你的选择不是“在 shell 里找浮点语法”而是“把计算交给谁”。2.2 bc任意精度、进制转换、数学函数的首选bc 是专门处理数字的命令行计算器最突出的能力是任意精度scale 决定除法结果保留几位小数echo 1.5 2.3 | bc # 3.8 echo scale4; 10 / 3 | bc # 3.3333 echo scale2; 1 / 3 | bc # .33 注意这里没有前导 0第二条命令输出 .33 而不是 0.33这是 bc 老用户都知道的小细节初看会懵一下文本拼接时尤其要留意。另一个容易误会的点是 scale 主要对除法这类需要产生小数位的运算生效加法和乘法按操作数自己的小数位数输出不要指望设了 scale2 之后所有结果都统一成两位。bc 还支持进制转换和数学库。加载 -l 选项后三角函数、对数、平方根都能用比如计算圆周率echo 4 * a(1) | bc -l进制转换则靠 ibase 和 obaseecho obase16; ibase10; 255 | bc # FF顺序建议先写 obase 再写 ibase避免 ibase 改变后后续数字的解析基准也跟着变这是我踩过的小坑。2.3 awk文本流里顺便就把算盘打了awk 是处理文本时最常见的浮点计算工具。它自带浮点数运算和格式化输出而且不需要像 bc 那样专门 echo 进去数据本来就在当前行里边提取边算是最顺手的姿势awk BEGIN { print 1 / 3 1 / 7 } awk BEGIN { printf %.2f\n, 10 / 3 } # 3.33awk 的浮点用 IEEE 754 双精度绝大多数场景足够。它最大的优势是“一行流”读日志、提取字段、求和平均数一条命令完成完全不用把值搬回 shell 再运算。第 4 章里我会给一个完整的日志耗时聚合案例就是基于这个特性。缺点是如果你追求精确到分的金额计算双精度会有二进制表示误差这时要么用 bc要么用 python 的 decimal 模块。2.4 python3复杂表达式和科学计算才值得请出来python3 -c 是很多人容易忽略的一条捷径适合表达式中带函数、随机数、循环等复杂逻辑python3 -c print(10 / 3) python3 -c import math; print(math.sqrt(2))python3 的精度和灵活性最高代价是启动时间明显比 bc 长依赖环境里也不一定装了 python3。在精简容器或嵌入式环境里能用 awk 就用 awk能用 bc 就用 bc不要为一句加法把 python3 拉进来。三个工具的取舍我通常这样记方案精度启动开销依赖适用场景$(( ))整数几乎为零无下标、百分比、位运算bc任意精度较低bc 包金融金额、进制转换、数学函数awk双精度浮点中基本预装文本行提取加聚合计算python3双精度/decimal高python3复杂表达式、科学计算这套判断标准能省掉大量“我在 shell 里怎么写浮点啊”的纠结先看数据在哪里再看机器上有什么最后看要什么精度。3. 十个我实际撞过的运算坑结果、原因、解法3.1 进制与解析类前导零、空值、非法数字第一个坑我每年都能碰到几次处理日期时尤其常见date 命令输出的月份可能是 08、09直接丢进算术展开会炸。因为以 0 开头的数字在 shell 算术里会被当作八进制08 不是合法八进制bash 直接报 value too great for base。month08 echo $((month 1)) # 报错 echo $((10#08 1)) # 9bash 里显式指定十进制10# 前缀是 bash 的写法不是所有 shell 都支持更稳妥的方案是把日期数值交给 awk 去处理awk 没有八进制困扰。第二个常踩的坑是空值静默变 0。前面提过算术上下文里未定义或空字符串直接当 0 处理结果不报错但可能掩盖真实问题。我运维的一台机器上就出过这种状况某个采集值偶发缺失脚本没有报错百分比却悄悄失真最终排查还是靠给变量加了显式默认值 ${v:-0} 才定位到缺失时机。第三个坑是非数字内容会导致语法错误而脚本如果没开 set -e这行报错后下一行还能继续执行非常隐蔽。aabc; echo $((a 1))直接报 syntax error但脚本并不会停在该处必须靠脚本整体的错误处理兜底。3.2 运算顺序与除零先乘后除、除以 0 的坑这个坑是最经典的used/total*100 与 used*100/total 看起来数学上没区别在 shell 里的结果天差地别。前者因为整数除法先执行used 小于 total 时直接得 00 再乘 100 还是 0后者先放大再除精度保住了。percent$((used / total * 100)) # 恒 0 percent$((used * 100 / total)) # 正确这条规则可以推广到所有整数除法先把被除数放大到需要的精度单位再除能少踩很多坑。注意放大时留意 64 位整数上限如果你要算的数是 TB 级别的放大 100 之后仍没超但放大 1000 万就可能越界。除零没有好办法只能提前判断。bash 里 $((1 / 0)) 会报 division by 0awk 里 1/0 部分环境输出 inf部分直接报浮点异常这两种结果放在监控告警里都很要命。写脚本的前置校验里加一句if (( divisor ! 0 ))成本极低收益极高。3.3 精度与输出类bc 的 scale 语义、IEEE754、字符串比较bc 的 scale 是截断不是四舍五入echo scale2; 2 / 3 | bc输出 .66不会进位成 .67。如果你要的是四舍五入老老实实用 printf 格式化或者在表达式中人为加半个精度单位再截断。我早期吃过这个亏以为 scale2 就是保留两位小数且四舍五入后来数据对不上账才发现是截断。浮点精度上awk 和 python 默认都是 IEEE 754 双精度。python3 里 0.1 0.2 输出 0.30000000000000004awk 直接 print 0.1 0.2 表面上输出 0.3 是因为它默认只打印有效位数你用 printf %.17g 一样能看到尾巴。这不是工具坏了是二进制的系统误差涉及钱的计算永远优先 bc 或 decimal。最后一个容易忽略的是字符串比较。if [ $a -gt $b ]只接受整数遇到浮点数直接报 integer expression expected。有人会用if [ $a \ $b ]做字符串比较那只会按字典序比0.2 会被错误地判定成小于 0.10。正规做法是交给 awk 判定awk -v a$a -v b$b BEGIN { exit !(a b) }这样真假由退出码承载shell 里的 if 直接跟在后面用即可。3.4 环境与格式类locale 干扰小数点、printf 格式化陷阱这个坑我遇到得晚但中过招之后就记牢了awk 解析数字可能受 LC_NUMERIC 影响在部分 locale 下小数点的语义会漂移。解决方式简单粗暴脚本开头export LC_ALLC把数字解析环境锁死顺带也能防止其他文本工具在非英语环境下的行为漂移。printf 的格式化字符串也值得单独说。printf $percent这种习惯是坏味道如果 percent 里碰巧有 %格式串会被二次解释。正确姿势是printf %s\n $percent要控制小数位数也必须是printf %.2f\n $num这样把格式本身写死。4. 两个可直接套用的真实场景磁盘告警与日志耗时聚合4.1 磁盘使用率百分比告警脚本先看场景每天定时巡检根分区使用率超过 80% 就告警。这个脚本把前面第 3 部分的整数乘除技巧完整用了一遍#!/bin/bash set -euo pipefail threshold80 df -P / | tail -n 2 | while read -r fs blocks used avail use mount; do pct${use%\%} if (( pct threshold )); then printf [%s] %s usage: %s%%\n $(date %F %T) $mount $pct fi donedf -P 输出是固定列加上后面的 tail 和 read 可以稳定拿到每个字段。use 这一列本身就带数字${use%%} 把百分号剥掉就是一个标准整数直接用 $(( )) 比较。如果你想凑整数乘除那部分逻辑也可以这样自己算row( $(df -P / | sed -n 2p) ) used$(( row[2] * 100 / row[1] ))先乘后除used_kb 乘 100 再除以 total_kb拿到百分比不会出现恒为 0 的问题。两种情况对比下来你会发现直接读 df 已经算好的列更省事但理解背后的整数乘除逻辑能让你在输出格式不满足需求时自己补计算。这里有个移植性小坑提醒一下df -P / | tail -n 2 | while read ...这种管道写法会让 while 在子 shell 里执行循环里改的变量外面看不到。如果后续要累计多个分区结果再统计就把管道换成进程替换 (df -P / | tail -n 2)或者先写临时文件。单分区简单判断场景影响不大但养成习惯能免掉一次困惑。4.2 日志里毫秒耗时转秒并求平均第二个场景是日志统计。假设应用日志长这样2026-01-05 10:00:01 taskload cost234ms 2026-01-05 10:00:02 taskload cost456ms你想算出这批请求的总耗时和平均耗时用 awk 就直接在读取过程里完成提取和聚合不需要把数字搬回 shell 再算一遍awk { for (i 1; i NF; i) { if ($i ~ /^cost/) { sub(/^cost/, , $i) sub(/ms$/, , $i) sum $i n } } } END { if (n 0) printf total%.3fs avg%.3fs n%d\n, sum / 1000, sum / n / 1000, n } app.log执行的输出是total0.690s avg0.345s n2这个例子的核心价值是日志字段往往混着标签和单位先删除前缀后缀得到纯数字再用 awk 的浮点累加。sum 本身是双精度除 1000 转成秒printf 控制到三位小数数据流完全在 awk 内部闭环不会因为在 shell 里反复命令替换而拖慢速度。如果统计完还要把结果喂给后续逻辑比如判断平均耗时超不超阈值可以再用awk -v把平均值传出来或者直接在外层通过命令替换接住字符串。我的建议是尽量让 awk 多做一点少跨一层进程监控脚本跑起来更稳当。4.3 容器与嵌入式环境里怎么降级真实生产环境里不一定有 bc也不一定有完整版 awk。我在精简 busybox 容器里遇到过一次只有 ash 加玩具级工具的情况浮点计算就绕不开两个选择要么把计算丢给宿主机的某个固定命令要么干脆在镜像里多打一个 bc 进去。busybox 的 awk 虽然够用但语法裁剪比较多如果脚本要在多平台分发提前做个能力探测会更稳command -v bc /dev/null 21 || command -v awk /dev/null 21 || echo need bc or awk虽然这行代码简单但能避免脚本在目标机器上直接翻车。嵌入式环境里连标准 awk 都未必有完整的有些方案是把精度计算放在宿主机统一完成设备端只传原始整数设计上减少对工具链的依赖。对于纯整数场景$(( )) 受制于内核和 shell 自身几乎所有 shell 都有反而是最稳妥的兜底。5. 收工我给自己的 .bashrc 加了什么5.1 一个顺手好用的 calc 函数交互环境下开个计算器不算难但每次打开 python3 还是有点重。我在 .bashrc 里放了这么个函数calc() { if [ $# -eq 0 ]; then printf usage: calc 1 2.3 * 4\n 2 return 1 fi awk BEGIN { printf \%.6g\\n\, ($*) } }用法就是calc 12.5 / 3.1输出 4.03226。选 awk 而不是 bc 的原因很简单绝大多数机器都有 awk而且交互场景不需要故意写 scale括号传参后计算语义跟数学表达式基本一致。缺点也有awk 的幂运算符是 ^ 不是 **把 Python 习惯带过来会报错这是我刚用这个函数时犯过的低级错误记在这里提醒各位。5.2 复杂计算切到 bc 的另一套封装需要更高精度或数学函数时我会切到另一个封装calc_bc() { echo scale10; $* | bc -l }calc_bc s(0.5)或者calc_bc e(2)都能直接出结果。直观对比一下交互环境算三角函数awk 得自己写展开式bc 一行就够但日常四则运算awk 的方式更快、更接近普通计算器的手感。我自己这些年写脚本的最高频组合是循环里用 $((...)) 处理索引和百分比提取文本时用 awk 边扫边算要求精度时再单独调 bc。三个工具各管一段反而比强行用一个工具做所有事情容易保持正确性。这些年我反复跟人强调的其实就三句话先乘后除保全整数精度浮点交给外部工具变量参与运算前先判空判格式。听着简单但每条都是从事故里换来的。