Shell 技巧 - 分解数字
上个月,我们继续深入探索 Apache Web 日志的隐秘角落,研究如何利用相对简单的 shell 脚本来生成有用且重要的数据。我们创建的特定脚本搜索了日志文件中前一天发生的流量,并汇总了传输的字节数。
这一切都很好,但与许多 shell 脚本一样,这个脚本也存在一些问题,当我繁忙的网站产生估计每月数据传输速率为 2346990660 字节时,这个问题立即变得显而易见。
显然,这是一个非常不友好的数字,而且没有逗号将其分成千、百万等等,就更加不友好了。更重要的是,在谈论数据传输时,我们习惯于以 2 的幂次方来思考,因此 1 千字节 (kilobyte) 是 1024 字节的数据,而不是 1000 字节的数据,而 1 兆字节 (megabyte) 是 1024 千字节的数据,依此类推。
不幸的是,我们用于数学计算的 expr 命令无法处理这些 2 的幂次方,因此,我们将不得不自己完成这项工作,将庞大的数字转换为更易读的 KB、MB 或 GB 值,视情况而定。
基本原理非常简单
kilo="$(( $value / 1024 ))" mega="$(( $kilo / 1024 ))" giga="$(( $mega / 1024 ))"
给定一个像 2346990660 这样的大数,结果就可以快速计算出来
$ sh -x convert.sh 2346990660 + value=2346990660 + kilo=2291983 + mega=2238 + giga=2 + exit 0
(提示:-x 选项可让您调试 shell 脚本,通过逐行显示正在执行的命令。)
当我们从一个大于 2GB 的大数切换到一个较小的值时,这种方法的问题立刻显现出来
$ sh -x convert.sh 5000 + value=5000 + kilo=4 + mega=0 + giga=0 + exit 0
我们不想要零值;我们想看到小数部分的值,这意味着我们不仅不能使用 shell 的内置数学功能,而且也不能使用 expr。相反,我们需要进入古老而又有些粗糙的 bc,即二进制计算器。
现在,bc 可能不太容易上手,但为了让您不必阅读手册页,下面介绍如何在除法结果小于 1.0 的情况下,强制结果保留小数点后四位。
$ echo "scale=2 ; 3000 / 30001" | bc .0999
您能明白如何将这些结合起来吗?这里有一种新的、经过大幅改进的方法来计算千 (kilo)、兆 (mega) 和 吉 (giga)。
$ sh -x convert.sh 5000 + value=5000 ++ echo 'scale=2; 5000 / 1024' ++ bc + kilo=4.88 ++ echo 'scale=2; 4.88 / 1024' ++ bc + mega=0 ++ echo 'scale=2; .00 / 1024' ++ bc + giga=0 + exit 0
我承认,-x 选项的调试输出在这里变得有点令人困惑,但您现在可以看到,当给定初始值 5000 字节时,kilo 设置为 4.88,而 mega 和 giga 都为零。
让我们再次尝试(为了清晰起见,从现在开始我将清理掉一些不必要的调试输出),使用最初的非常大的值
$ convert.sh 2346990660 value=2346990660 kilo=2291983.06 mega=2238.26 giga=2.18
酷。现在我们终于可以看到,我们每月从网站传输大约 2.18GB 的数据——这比之前显示的巨大数值要连贯得多。
现在,让我们弄清楚如何始终显示这些值中最合乎逻辑的一个,而不是全部都显示。
找出哪个值最佳的最简单方法是确定该值何时降至 1.0 以下。对于 5000 字节的情况,最好显示为 4.88KB,而对于大数 (bignum) 值,则为 2.18GB。
为了弄清楚该值何时降至零以下,我们很希望进行浮点数数值比较,但遗憾的是,shell 无法做到这一点。如果您尝试这样做,您只会收到“integer expression expected”(应为整数表达式)的错误。
有很多方法可以获取值的“floor”(向下取整),但我再次使用 bc 来完成这项工作,方法是再次计算除法,这次完全不使用任何 scale 值。
kiloint=$( echo "$value/1024" | bc)"
这样做只会得到 $kilo 值的整数部分,这确实可以在条件语句中进行测试。
if [ $kiloint -lt 1 ] ; then
现在,将所有内容放在一起,下面是脚本的样子
kilo=$( echo "scale=2; $value / 1024" | bc ) kiloint=$( echo "$value / 1024" | bc ) mega=$( echo "scale=2; $kilo / 1024" | bc ) megaint=$( echo "$kilo / 1024" | bc ) giga=$( echo "scale=2; $mega / 1024" | bc ) gigaint=$( echo "$mega / 1024" | bc ) if [ $kiloint -lt 1 ] ; then echo "$value bytes" elif [ $megaint -lt 1 ] ; then echo "${kilo}KB" elif [ $gigaint -lt 1 ] ; then echo "${mega}MB" else echo "${giga}GB" fi
有点奇怪,但它确实完全按照我们希望的方式工作。
$ sh convert.sh 5000000000 4.65GB $ sh convert.sh 5000000 4.76MB $ sh convert.sh 50000 48.82KB $ sh convert.sh 50 50 bytes
最后一步是将其设为一个函数,以便我们可以将其包含在其他 shell 脚本中并根据需要访问它。这在 Bourne Shell 中通过给它一个唯一的名称,然后将功能代码用花括号括起来来完成。
kmg() { code for function goes here, params are $1, $2, etc. }
然后可以在 shell 脚本中按名称调用它(k=千字节, m=兆字节, g=吉字节)
kmg 500000
更重要的是,您可以使用子 shell 表示法将其嵌入到一行中,因此,给定 kmg() 函数,以下两行脚本可以完美地工作。
echo given value is $1 echo which converts to $(kmg $1)
这既简洁又简短,如果 kmg 函数被放入其自己的文件中,您还可以使用 . 命令在 shell 脚本中包含另一个文件,这意味着整个测试脚本现在是
#!/bin/sh . kmg.sh echo The given value $1 bytes = $(kmg $1) exit 0
我在这里没有空间了,但我希望您能看到这种方法如何应用于各种不同的 shell 任务,使您的 shell 脚本更高效,编写速度也更快!
Dave Taylor 是一位拥有 26 年 UNIX 经验的资深人士,The Elm Mail System 的创建者,以及最近畅销书的作者 Wicked Cool Shell Scripts 和 Teach Yourself Unix in 24 Hours,以及他的其他 16 本技术书籍。他的主要网站是 www.intuitive.com。