常年在终端里用 bash 写脚本的人,早晚会撞上一堵墙:$((...))做整数计算很顺手,但一旦涉及小数、百分比、单位换算,就开始各种翻车——除法直接截断、精度对不上、跨平台行为还不一样。最开始我习惯用 awk 顶一下,可为了算一个表达式去写一段 awk,总有种杀鸡用牛刀的别扭。后来把 bc 彻底用顺了,才发现这个老牌计算器才是 bash 数学运算的真正补完形态。这篇文章就围绕 bc 的核心用法、实际脚本场景和文档里不会明说的坑,聊聊怎么把它用顺手。
1. 为什么 bash 内置算术会在真实场景里拉胯
很多人第一次用 bash 写统计脚本,都会先试$((a+b))、$((a*b))这种算术扩展。做整数加减乘确实没问题,但一旦遇到除法,就会第一次体会到什么叫“只管杀不管埋”:echo $((3/2))的结果是 1,不是 1.5,小数部分直接被丢掉。这个行为不只是某个发行版的问题,而是 bash 算术扩展的语言设计——它只做整数运算,没有浮点类型。
如果你往回翻资料,会发现 GNU 项目的 bash 手册里写得很清楚:shell 算术的求值结果永远是整数,除法对负数采用类似 truncate 而不是向下取整的策略,模运算的语义在某些边界也和数学直觉对不上。这种设计在 C 语言里是合理的,因为 C 是编译型语言,有明确的类型声明;但放在交互式脚本里就非常难受——你要算个平均耗时、带宽、内存占用百分比,根本绕不开小数。
1.1 整数除法截断带来的连锁问题
整数截断最典型的危害是会“传染”。假设你要算一段日志里的错误率:
errors=12 total=1000 rate=$((errors * 100 / total)) echo "$rate%" # 输出 1%这里 12 / 1000 = 0.012,但整数运算下乘以 100 再除以 total,等于 1200 / 1000,结果是 1,看起来误差不大。可一旦换成 errors=3、total=7,结果就完全不能看了:3 * 100 / 7 = 42,而真实值应该是 42.857...,42% 和实际 42.86% 差异倒还好,但如果你用同样的逻辑去算成功率,4/7变成 57%,四舍五入的误差就开始出现。
在长时间统计脚本里,这种误差还会不断累计。比如你每小时采集一次 CPU 用量,把 24 个整数百分比相加再算平均值,因为每次单个运算都被截断,24 次截断误差叠加后,最终平均值偏差就不是一两个百分点的问题了。
1.2 expr、declare -i 与 awk 临时方案的各自代价
除了$((...)),老派写法还会用expr。expr 3 / 2同样是整数除法,输出 1。而且expr的语法特别拧巴,乘号要用\*转义,参数之间必须空格隔开,看多了真的会怀疑人生。declare -i声明整数变量也一样,没解决浮点问题。
awk 确实能做浮点运算,精度也不差,比如awk "BEGIN{print 3/2}"能直接输出 1.5。awk 的问题在于它是一整套文本处理语言,为了一个表达式把整个 awk 进程拉起来,总有点重。而且 awk 默认的printf格式、科学计数法的显示方式和 bc 不同,处理进制转换需要自己写循环,比 bc 繁琐很多。
1.3 bc 的身份定位:一个可脚本化的任意精度计算器
bc 的完整名字是 Basic Calculator,但它远不是计算器三个字能概括的。它是一个带完整编程语言语法的任意精度数值计算工具,支持变量、函数、条件、循环、数组,甚至自定义函数。默认情况下连浮点数也支持,只是需要显式设置scale来告诉它“我要保留几位小数”。bc 的历史可以追溯到几十年前的 Unix 系统,现在每个主流 Linux 发行版都默认包含,macOS 也一样,可以说比许多装来装去的工具更接近“系统自带”的定位。
把 bc 放进 bash 脚本,本质上就是把高精度计算能力交给最合适的工具,而 bash 只负责外壳、变量和流程控制。这种组合是我用下来最稳的 python 或 perl 之外的选择——进程启动有一定开销,但胜在轻、快、无依赖。
2. bc 入门实操:从管道、scale 到变量传参
2.1 最简单的管道调用方式
bc 默认从标准输入读取表达式,所以最自然的配合就是 echo 管道:
echo "1 + 2" | bc # 3把表达式用双引号保护起来,避免被 bash 展开。如果想算浮点数,必须设置scale:
echo "scale=2; 10 / 3" | bc # 3.33这里scale表示小数点后保留多少位。默认值是 0,所以如果你直接echo "10 / 3" | bc,会得到整数 3。很多新手第一次用 bc 发现“小数没了”,基本都是忘了scale。这个行为和直觉有偏差,但一旦理解了 bc 的模型——加法、乘法默认不会截断,除法、取余、乘幂这些结果受scale影响——就不会再踩。
2.2 scale、ibase/obase 与表达式语法
bc 的语法看起来像 C 语言和 shell 的混合体。可以在一条表达式里用分号分隔多个语句:
echo "scale=4; 1 / 3; 2 / 3" | bc # .3333 # .6666注意这里输出会有前导点,因为没有写整数部分的 0,这是 bc 的传统显示方式,不影响被 bash 读取后转为浮点数。如果你对输出格式有洁癖,可以后续用printf处理。
scale除了影响除法,还影响幂运算和平方根。sqrt(x)的结果精度受scale控制,不过使用-l选项时,scale会自动设置为 20,这样很多数学函数的输出会更精确。
进制转换用ibase(输入进制)和obase(输出进制):
echo "ibase=16; FF" | bc # 255 echo "obase=2; 255" | bc # 11111111 echo "ibase=8; 17" | bc # 15这组命令在做 IPv6 地址转换、权限掩码计算、配置项格式转换时非常实用。但这里有一个经典的坑:如果你同时设置ibase和obase,顺序会影响结果。原因在于 bc 在解析赋值语句时,ibase和obase的增量本身是按当前进制解析的。
举个反直觉的例子:
echo "ibase=16; obase=2; FF" | bc会得到 11111111,看起来正常。但echo "ibase=16; obase=10; FF" | bc却可能输出 255?其实还是 255。问题出在如果你先设置ibase=16,再写obase=16,bc 会把 16 当作十六进制解释,实际就是 22。所以安全的写法是先设obase再设ibase,或者利用ibase=F、obase=10这种技巧绕开歧义。
2.3 在 bash 脚本里把数值传给 bc 的几种方式
最直接的传参方式是变量插值:
price=42.5 count=3 echo "scale=2; $price * $count" | bc # 127.50在双引号环境里,bash 会先把$price和$count展开成字面量,bc 再去解析。这里要特别注意:如果变量来自外部输入,包含空格、特殊符号或意外字符,表达式就会被 bc 解析成莫名其妙的语法错误,甚至可能带来注入风险。永远不要在没做校验的情况下,直接把read得到的值塞进 bc 表达式。
推荐做法是先用正则或case做一次“是否合法数字”的检查:
if [[ ! $raw =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then echo "非数字输入: $raw" >&2 exit 1 fi校验通过后再拼表达式,能避免很多幽灵问题。另一种传参方式是用 here-string:
read -r result <<< "scale=2; 7 * 8 / 3" bc <<< "scale=2; 7 * 8 / 3"<<<语法会创建一个临时字符串作为 bc 的标准输入,比 echo 管道更简洁,唯一的缺点是某些很老的 shell 不一定支持,但在现代 Linux 环境下基本不用担心。
如果需要传多个变量,而且里面有引号、反斜杠之类的特殊字符,用 heredoc 更稳:
bc << EOF scale=2 a = $a b = $b a + b a * b EOFheredoc 里允许写多行 bc 语句,还支持注释(#开头),对于复杂逻辑的可读性提升非常明显。后面第 5 节还会专门讲 heredoc 的坑。
3. 实战场景:用 bc 改写真实统计与换算脚本
3.1 计算平均值、方差、百分比
既然治好了小数焦虑,第一个能用上的地方就是统计汇总。比如你在性能测试或日志采集脚本里拿到一组耗时数据:
t1=1.23 t2=1.41 t3=1.52 avg=$(echo "scale=3; ($t1 + $t2 + $t3) / 3" | bc) echo "平均耗时: ${avg}s"表达式里用括号分组,因为 bc 的优先级和大部分语言一致。更复杂的方差计算可以拆成两步:先算平均值,再算每个样本与平均值的差的平方和,最后除以样本数。每一步都交给 bc,但注意最终结果也会截断到scale指定的位数,所以要多保留一两位用于后续计算。
百分比计算是另一个高频场景。比如脚本里要判断磁盘或内存使用率是否超过阈值:
used=7340032 total=8388608 percent=$(echo "scale=1; $used * 100 / $total" | bc) echo "使用率: ${percent}%" if (( $(echo "$percent >= 80" | bc) )); then echo "告警:使用率过高" fi这里有个小技巧:bc 的比较表达式会输出 1(真)或 0(假),放进$((...))或者if条件里都能被正确识别。但是注意,$(echo ... | bc)输出的是 0 或 1,字符串转数字的过程没有问题。如果你直接if (( $(echo ... ) )),实际上是把 bc 的输出当成算术扩展重新计算,1 就是真,0 就是假,可行但多了一步。我更喜欢写成if [[ $(echo ... | bc) -eq 1 ]],至少在可读性上不会让人困惑。
3.2 字节、KB、MB 自动换算
单位换算脚本是我用 bc 最多的场景之一。服务器日志里大部分体积数据都是字节,要变成人类可读的 KB、MB、GB,如果只用 bash 整数除,很容易出现“1023 字节显示成 0 KB”的尴尬。bc 可以精确处理临界值:
size_bytes=1536000 echo "scale=2; $size_bytes / 1024 / 1024" | bc # 1.46 MB更通用一点,可以写一个函数放到.bashrc或独立脚本里:
human_size() { local bytes="$1" if (( bytes < 1024 )); then echo "${bytes}B" else echo "$(echo "scale=2; $bytes / 1024" | bc)KB" fi }这个函数只处理 KB 层级,再往上就得嵌套判断。我自己常用的版本是用if链做 4 级分支,每个分支都用 bc 算一次输出,速度和可读性都还可以。如果你要处理超大文件,$((bytes / 1024 / 1024))的整数溢出风险会增大,bc 的高精度就更有用了。
3.3 循环累加与复杂表达式
循环里反复调用 bc 是最容易写出“慢脚本”的方式。比如:
sum=0 for n in $(seq 1 1000); do sum=$(echo "$sum + $n" | bc) done这个写法逻辑没错,但每循环一次就 fork 一个进程,1000 次循环大约会多出几十毫秒到上百毫秒的损耗,在更复杂的脚本里会被放大。更聪明的办法是把表达式在 bash 端拼接好,最后只调用一次 bc:
expr="0" for n in $(seq 1 1000); do expr="${expr} + ${n}" done result=$(echo "$expr" | bc)如果表达式太长,bash 命令行长度的上限可能会成为问题——但日常脚本很少会超。另一个做法是用 heredoc 一次性把所有计算步骤全丢给 bc:
bc << EOF sum = 0 for (i = 1; i <= 1000; i++) sum += i sum EOFbc 自己支持 for 循环和变量,所以这种写法等于把计算逻辑完全交给 bc,bash 只负责收集结果。我建议小逻辑用管道,量大或逻辑复杂的用 heredoc,兼顾性能和可读性。
4. 深入 bc 语言:函数、进制、科学计数与浮点深入
4.1 内置数学函数与自定义函数
bc 不加任何参数时只有基础四则运算。加了-l参数后,会加载标准数学库,同时暴露几个函数:s(x)正弦、c(x)余弦、a(x)反正切、l(x)自然对数、e(x)指数,以及j(n,x)贝塞尔函数。注意三角函数的参数是弧度制。比如你要算 30 度的正弦:
echo "scale=10; s(3.14159265358979323846 / 6)" | bc -l # .5000000000-l会把scale设为 20,所以输出精度高了不少。更遗憾的是,很多人以为有了-l就会有sqrt函数,但只要你没加载数学库,sqrt也是不可用的;用-l之后,sqrt(2)才会输出精确到 20 位的结果。如果没加载,bc 会直接报语法错误。
你也可以用define关键字写自己的函数。比如实现一个简单的四舍五入函数:
bc << 'EOF' define round(x, n) { auto old_scale old_scale = scale scale = n + 1 y = x / 1 scale = 0 if ((y * 10) % 10 >= 5) { y = y + 1 } scale = old_scale return y } round(2.675, 2) EOF这个函数写得比较粗糙,只在正数场景下可靠,但已经能看出 bc 的函数和变量结构跟 C 语言非常接近。auto关键字用来声明局部变量,这点在文档里不难找到,但实际用到的人很少。
4.2 ibase/obase 进制转换的深入细节
进制转换不只是简单的“16 进制 FF 转 255”。在运维脚本里,常常需要把权限掩码、网络掩码、配置文件里的十六进制数值批量转换。bc 的ibase和obase可以支持 2 到 16 进制,足够覆盖大多数场景:
echo "obase=2; 192" | bc # 11000000 echo "ibase=2; 11000000" | bc # 192 echo "ibase=16; obase=10; 1F" | bc # 31第一个命令为什么obase=2; 192就可以?因为赋值obase=2时,ibase还是默认的十进制,所以 2 就是十进制的 2,后面 192 也按十进制解析。这是正确的顺序。如果你反过来写ibase=16; obase=2; FF,理论上也能工作,但它在某些 bc 版本里会变慢甚至报错,因为 ibase 的改变会影响后面所有整数的解析。为了统一,我一直坚持“先 obase,后 ibase”的书写顺序。
从二进制转十进制再到十六进制,可以链式调用,也可以一步到位:
echo "ibase=2; obase=16; 11111111" | bc # FF这个命令先设obase=16,再设ibase=2,后面 11111111 按二进制读入,输出为十六进制 FF。非常巧妙,但前提还是顺序要对。
4.3 精度控制:舍入、截断与比较
bc 的scale决定了除法结果保留几位小数,但它是“截断”而不是“四舍五入”。比如scale=2; 1/6结果是.16,因为 0.1666... 被截断到两位。如果你希望显示圆整值,通常做法是先提高scale,再用printf做最终格式化:
raw=$(echo "scale=4; 1 / 6" | bc) # .1666 printf "%.2f\n" "$raw" # 0.17这里printf的%.2f才是四舍五入,bc 只负责提供足够精度。负数场景下,bc 的截断方向是朝 0,比如scale=2; -1/6是-.16,而标准数学意义上的“向下舍入”应该是 -0.17,二者有差异。在账单、税率计算这类对舍入方向敏感的场景,建议用printf兜底,或者写自定义 round 函数。
比较运算方面,bc 支持>、<、>=、<=、==、!=,输出 1 或 0。浮点比较不能完全依赖==,因为二进制浮点的表示误差会导致0.1 + 0.2 == 0.3在 bc 里可能返回 0。如果你需要判断是否近似相等,可以计算两个数的差,再和一个小阈值比较:
diff=$(echo "scale=6; ($a - $b)" | bc) if (( $(echo "scale=6; if ($diff < 0.000001 && $diff > -0.000001) 1 else 0" | bc) )); then echo "近似相等" fi这样虽然啰嗦,但足够可靠。bc 没有结构体、没有十进制浮点和 IEEE 浮点,所以一切都要显式管理。
5. 提高鲁棒性:输入校验、heredoc、性能边界与替代选择
5.1 heredoc 与 here-string 的正确使用方式
heredoc 是把多行 bc 代码喂给 bc 的最佳方式,既能保持格式,又能写注释。但要注意默认情况下 heredoc 里的变量会被 bash 展开,所以如果你只是传一段静态表达式,用带引号的<<'EOF'更安全:
bc << 'EOF' scale = 2 # 静态计算公式 (3 + 5) / 2 EOF # 输出 4如果你确实需要变量插值,就让 heredoc 不带引号,但此时要格外小心$、\、反引号在 shell 和 bc 两层解析下的意义。一个常见翻车点是变量里有$符号,比如价格$9.99,放进双引号 heredoc 后$9会被 shell 当作变量名解析,结果变成空串。解决方法是先把变量里的特殊字符处理掉,或者直接转义。我通常更倾向用 here-string<<<配合变量插值,因为只传一行表达式时,这里展开的机率更小。
5.2 容易被忽略的坑:scale 全局影响与命令替换的尾随换行
scale是一个全局变量,它会影响脚本后面所有需要精度的运算。有时候你在一段脚本的前面设置了scale=6,后面做乘法可能不受影响,但再做除法时结果突然精确到 6 位,导致比较运算也变了。所以每个独立脚本,我都会在调用 bc 的代码块里显式重置scale,比如scale=4; ...,而不是依赖上一次调用留下的值。
命令替换$(...)会把 bc 输出的尾随换行吃掉,这就带来一个小问题:如果 bc 输出是多行的,命令替换会拼成一行。比如第 2 节那个多行输出例子:
echo "$(echo 'scale=4; 1/3; 2/3' | bc)" # .3333 .6666两个结果会挤到同一行。如果你需要保留多行,建议直接用管道输出到变量外的临时文件,或者用 mapfile 读入数组。这种问题对大多数脚本影响不大,但见过有人因此写出的配置文件格式错乱,所以提一下。
另一个更隐蔽的问题:bc 的除法在scale=0时会用整数除,但取模运算%在浮点下的行为可能不符合预期。scale=2; 5 % 2会输出 0,但scale=2; 5.5 % 2输出 1.50,这和数学里的“浮点余数”定义一致,但如果你期待的是类似整数余数的东西,就要先转换。
5.3 性能实测:bc 的进程开销与 awk/python 的取舍
我简单压过一次调用成本。在同一台普通 Linux 主机上,连续 1000 次echo "1+1" | bc大约耗时 0.2 秒上下,也就是单次约 0.2 毫秒;而awk "BEGIN{print 1+1}"单次大约 0.3 毫秒。差别不算大,但一旦循环到一万次,bc 版本可能要 2 秒以上,脚本整体就肉眼可见地变慢。因此,如果只是少量几次运算,bc 完全没有问题;但涉及上万行数据的逐行计算,我更推荐一次性交给 awk 处理,awk 的浮点精度虽然不如 bc,但对 99% 的统计场景已经足够。
如果脚本本身已经依赖 python,那直接用 python 做数值计算当然更灵活,还能处理正则、文件、网络等杂活。bc 的优势在于它几乎不存在于第三方依赖,任何 Unix 环境都能跑,而且对系统资源占用极小。我在生产环境里更倾向把 bc 和系统内置命令组合使用,避免为了一个小脚本去安装或依赖额外运行环境。
5.4 输入校验与错误输出处理
最后再强调一次输入校验。bc 遇到非法表达式不会像 python 那样抛异常,而是输出一条错误信息到 stderr,同时返回码非 0。在 bash 脚本里,默认不会因为 stderr 有输出就失败,所以你要手动检查:
result=$(echo "scale=2; $var * 1.1" | bc 2>/dev/null) if ! [[ $result =~ ^[0-9]+(\.[0-9]+)?$ ]]; then echo "计算失败,原始输入: $var" >&2 exit 1 fi这样可以把 bc 无法解析的输入提前拦截。注意正则要允许负数时,改用^-?[0-9]+(\.[0-9]+)?$。这是一个我在真实生产脚本里踩过的坑——当时没有校验,某个字段里混入了字母,结果 bc 输出空串,bash 变量变成空值,后面整段逻辑全部错乱了。
写在最后的一点个人体会
从我自己的使用曲线来看,第一次用 bc 是被整数除法逼的,后来逐渐发现了-l、进制转换和 heredoc 这些特性,才真正把它当成 bash 工具箱里的一等公民。最流畅的用法往往是“bash 管输入输出,bc 管计算,如非必要不把计算逻辑写进 bash 的算术扩展”。在性能不太敏感的脚本里,宁可多用一次 bc 进程,也不要为了省那点开销去硬写整数近似,毕竟计算的正确性才是第一位的。最后一个小建议:如果你经常在同一个脚本里重复使用相同精度的计算,可以在文件头部定义一个 shell 函数,比如calc() { echo "scale=4; $*" | bc; },之后所有表达式都能直接calc "1/3"这样调用,既省心也减少拼写错误。