Shell 技巧 - 分解数字

作者:Dave Taylor

上个月,我们继续深入探索 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 ScriptsTeach Yourself Unix in 24 Hours,以及他的其他 16 本技术书籍。他的主要网站是 www.intuitive.com

加载 Disqus 评论