12.8. 数学计算命令

"Doing the numbers"

factor

将一个正数分解为多个素数.

 bash$ factor 27417
 27417: 3 13 19 37
 	      

bc

Bash 不能处理浮点运算, 并且缺乏特定的一些操作,这些操作都是一些重要的计算功能.幸运的是, bc 可以解决这个问题.

bc 不仅仅是个多功能灵活的精确的工具, 而且它还提供许多编程语言才具备的一些方便的功能.

bc 比较类似于 C 语言的语法.

因为它是一个完整的 UNIX 工具, 所以它可以用在管道中, bc 在脚本中也是很常用的.

这里有一个简单的使用 bc 命令的模版可以用来在计算脚本中的变量. 用在命令替换 中.

 	      variable=$(echo "OPTIONS; OPERATIONS" | bc)
 	      


Example 12-42. 按月偿还贷款

   1 #!/bin/bash
   2 # monthlypmt.sh: 计算按月偿还贷款的数量.
   3 
   4 
   5 #  这份代码是一份修改版本, 原始版本在 "mcalc" (贷款计算)包中,
   6 #+ 这个包的作者是 Jeff Schmidt 和 Mendel Cooper (本书作者).
   7 #   http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz  [15k]
   8 
   9 echo
  10 echo "Given the principal, interest rate, and term of a mortgage,"
  11 echo "calculate the monthly payment."
  12 
  13 bottom=1.0
  14 
  15 echo
  16 echo -n "Enter principal (no commas) "
  17 read principal
  18 echo -n "Enter interest rate (percent) "  # 如果是 12%, 那就键入 "12", 别输入 ".12".
  19 read interest_r
  20 echo -n "Enter term (months) "
  21 read term
  22 
  23 
  24  interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # 转换成小数.
  25                  # "scale" 指定了有效数字的个数.
  26   
  27 
  28  interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc)
  29  
  30 
  31  top=$(echo "scale=9; $principal*$interest_rate^$term" | bc)
  32 
  33  echo; echo "Please be patient. This may take a while."
  34 
  35  let "months = $term - 1"
  36 # ==================================================================== 
  37  for ((x=$months; x > 0; x--))
  38  do
  39    bot=$(echo "scale=9; $interest_rate^$x" | bc)
  40    bottom=$(echo "scale=9; $bottom+$bot" | bc)
  41 #  bottom = $(($bottom + $bot"))
  42  done
  43 # ==================================================================== 
  44 
  45 # -------------------------------------------------------------------- 
  46 #  Rick Boivie 给出了一个对上边循环的修改,
  47 #+ 这个修改更加有效率, 将会节省大概 2/3 的时间.
  48 
  49 # for ((x=1; x <= $months; x++))
  50 # do
  51 #   bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc)
  52 # done
  53 
  54 
  55 #  然后他又想出了一个更加有效率的版本,
  56 #+ 将会节省 95% 的时间!
  57 
  58 # bottom=`{
  59 #     echo "scale=9; bottom=$bottom; interest_rate=$interest_rate"
  60 #     for ((x=1; x <= $months; x++))
  61 #     do
  62 #          echo 'bottom = bottom * interest_rate + 1'
  63 #     done
  64 #     echo 'bottom'
  65 #     } | bc`       # 在命令替换中嵌入一个 'for 循环'.
  66 # --------------------------------------------------------------------------
  67 #  On the other hand, Frank Wang suggests:
  68 #  bottom=$(echo "scale=9; ($interest_rate^$term-1)/($interest_rate-1)" | bc)
  69 
  70 #  因为 . . .
  71 #  在循环后边的算法
  72 #+ 事实上是一个等比数列的求和公式.
  73 #  求和公式是 e0(1-q^n)/(1-q),
  74 #+ e0 是第一个元素 并且 q=e(n+1)/e(n)
  75 #+ 和 n 是元素的数量.
  76 # --------------------------------------------------------------------------
  77 
  78 
  79  # let "payment = $top/$bottom"
  80  payment=$(echo "scale=2; $top/$bottom" | bc)
  81  # 使用2位有效数字来表示美元和美分.
  82  
  83  echo
  84  echo "monthly payment = \$$payment"  # 在总和的前边显示美元符号.
  85  echo
  86 
  87 
  88  exit 0
  89 
  90 
  91  # 练习:
  92  #   1) 处理输入允许本金总数中的逗号.
  93  #   2) 处理输入允许按照百分号和小数点的形式输入利率.
  94  #   3) 如果你真正想好好编写这个脚本,
  95  #      那么就扩展这个脚本让它能够打印出完整的分期付款表.


Example 12-43. 数制转换

   1 #!/bin/bash
   2 ##########################################################################
   3 # 脚本       :	base.sh - 用不同的数值来打印数字 (Bourne Shell)
   4 # 作者       :	Heiner Steven (heiner.steven@odn.de)
   5 # 日期       :	07-03-95
   6 # 类型       :	桌面
   7 # $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
   8 # ==> 上边这行是 RCS ID 信息.
   9 ##########################################################################
  10 # 描述
  11 #
  12 # Changes
  13 # 21-03-95 stv	fixed error occuring with 0xb as input (0.2)
  14 ##########################################################################
  15 
  16 # ==> 在本书中使用这个脚本通过了作者的授权.
  17 # ==> 注释是本书作者添加的.
  18 
  19 NOARGS=65
  20 PN=`basename "$0"`			       # 程序名
  21 VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2`  # ==> VER=1.2
  22 
  23 Usage () {
  24     echo "$PN - print number to different bases, $VER (stv '95)
  25 usage: $PN [number ...]
  26 
  27 If no number is given, the numbers are read from standard input.
  28 A number may be
  29     binary (base 2)		starting with 0b (i.e. 0b1100)
  30     octal (base 8)		starting with 0  (i.e. 014)
  31     hexadecimal (base 16)	starting with 0x (i.e. 0xc)
  32     decimal			otherwise (i.e. 12)" >&2
  33     exit $NOARGS 
  34 }   # ==> 打印出用法信息的函数.
  35 
  36 Msg () {
  37     for i   # ==> 省略 [list] .
  38     do echo "$PN: $i" >&2
  39     done
  40 }
  41 
  42 Fatal () { Msg "$@"; exit 66; }
  43 
  44 PrintBases () {
  45     # 决定数值的数制
  46     for i      # ==> 省略 [list]...
  47     do         # ==> 所以是对命令行参数进行操作.
  48 	case "$i" in
  49 	    0b*)		ibase=2;;	# 2进制
  50 	    0x*|[a-f]*|[A-F]*)	ibase=16;;	# 16进制
  51 	    0*)			ibase=8;;	# 8进制
  52 	    [1-9]*)		ibase=10;;	# 10进制
  53 	    *)
  54 		Msg "illegal number $i - ignored"
  55 		continue;;
  56 	esac
  57 
  58 	# 去掉前缀, 将16进制数字转换为大写(bc需要大写)
  59 	number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
  60 	# ==>使用":" 作为sed分隔符, 而不使用"/".
  61 
  62 	# 将数字转换为10进制
  63 	dec=`echo "ibase=$ibase; $number" | bc`  # ==> 'bc' 是个计算工具.
  64 	case "$dec" in
  65 	    [0-9]*)	;;			 # 数字没问题
  66 	    *)		continue;;		 # 错误: 忽略
  67 	esac
  68 
  69 	# 在一行上打印所有的转换后的数字.
  70 	# ==> 'here document' 提供命令列表给'bc'.
  71 	echo `bc <<!
  72 	    obase=16; "hex="; $dec
  73 	    obase=10; "dec="; $dec
  74 	    obase=8;  "oct="; $dec
  75 	    obase=2;  "bin="; $dec
  76 !
  77     ` | sed -e 's: :	:g'
  78 
  79     done
  80 }
  81 
  82 while [ $# -gt 0 ]
  83 # ==>  这里必须使用一个 "while 循环",
  84 # ==>+ 因为所有的 case 都可能退出循环或者
  85 # ==>+ 结束脚本.
  86 # ==> (感谢, Paulo Marcel Coelho Aragao.)
  87 do
  88     case "$1" in
  89 	--)     shift; break;;
  90 	-h)     Usage;;                 # ==> 帮助信息.
  91 	-*)     Usage;;
  92          *)     break;;			# 第一个数字
  93     esac   # ==> 对于非法输入更严格检查是非常有用的.
  94     shift
  95 done
  96 
  97 if [ $# -gt 0 ]
  98 then
  99     PrintBases "$@"
 100 else					# 从标准输入中读取
 101     while read line
 102     do
 103 	PrintBases $line
 104     done
 105 fi
 106 
 107 
 108 exit 0
 

调用 bc 的另一种可选的方法就是使用 here document ,并把它嵌入到 命令替换 块中. 当一个脚本需要将一个选项列表和多个命令传递到 bc 中时, 这种方法就显得非常合适.

   1 variable=`bc << LIMIT_STRING
   2 options
   3 statements
   4 operations
   5 LIMIT_STRING
   6 `
   7 
   8 ...or...
   9 
  10 
  11 variable=$(bc << LIMIT_STRING
  12 options
  13 statements
  14 operations
  15 LIMIT_STRING
  16 )


Example 12-44. 使用 "here document" 来调用 bc

   1 #!/bin/bash
   2 # 使用命令替换来调用 'bc' 
   3 # 并与 'here document' 相结合.
   4 
   5 
   6 var1=`bc << EOF
   7 18.33 * 19.78
   8 EOF
   9 `
  10 echo $var1       # 362.56
  11 
  12 
  13 #  $( ... ) 这种标记法也可以.
  14 v1=23.53
  15 v2=17.881
  16 v3=83.501
  17 v4=171.63
  18 
  19 var2=$(bc << EOF
  20 scale = 4
  21 a = ( $v1 + $v2 )
  22 b = ( $v3 * $v4 )
  23 a * b + 15.35
  24 EOF
  25 )
  26 echo $var2       # 593487.8452
  27 
  28 
  29 var3=$(bc -l << EOF
  30 scale = 9
  31 s ( 1.7 )
  32 EOF
  33 )
  34 # 返回弧度为1.7的正弦.
  35 # "-l" 选项将会调用 'bc' 算数库.
  36 echo $var3       # .991664810
  37 
  38 
  39 # 现在, 在函数中试一下...
  40 hyp=             # 声明全局变量.
  41 hypotenuse ()    # 计算直角三角形的斜边.
  42 {
  43 hyp=$(bc -l << EOF
  44 scale = 9
  45 sqrt ( $1 * $1 + $2 * $2 )
  46 EOF
  47 )
  48 # 不幸的是, 不能从bash 函数中返回浮点值.
  49 }
  50 
  51 hypotenuse 3.68 7.31
  52 echo "hypotenuse = $hyp"    # 8.184039344
  53 
  54 
  55 exit 0


Example 12-45. 计算圆周率

   1 #!/bin/bash
   2 # cannon.sh: 通过开炮来取得近似的圆周率值.
   3 
   4 # 这事实上是一个"Monte Carlo"蒙特卡洛模拟的非常简单的实例:
   5 #+ 蒙特卡洛模拟是一种由现实事件抽象出来的数学模型,
   6 #+ 由于要使用随机抽样统计来估算数学函数, 所以使用伪随机数来模拟真正的随机.
   7 
   8 #  想象有一个完美的正方形土地, 边长为10000个单位.
   9 #  在这块土地的中间有一个完美的圆形湖,
  10 #+ 这个湖的直径是10000个单位.
  11 #  这块土地的绝大多数面积都是水, 当然只有4个角上有一些土地.
  12 #  (可以把这个湖想象成为使这个正方形的内接圆.)
  13 #
  14 #  我们将使用老式的大炮和铁炮弹
  15 #+ 向这块正方形的土地上开炮.
  16 #  所有的炮弹都会击中这块正方形土地的某个地方.
  17 #+ 或者是打到湖上, 或者是打到4个角的土地上.
  18 #  因为这个湖占据了这个区域大部分地方,
  19 #+ 所以大部分的炮弹都会"扑通"一声落到水里.
  20 #  而只有很少的炮弹会"砰"的一声落到4个
  21 #+ 角的土地上.
  22 #
  23 #  如果我们发出的炮弹足够随机的落到这块正方形区域中的话,
  24 #+ 那么落到水里的炮弹与打出炮弹的总数的比率,
  25 #+ 大概非常接近于 PI/4.
  26 #
  27 #  原因是所有的炮弹事实上都
  28 #+ 打在了这个土地的右上角,
  29 #+ 也就是, 笛卡尔坐标系的第一象限.
  30 #  (之前的解释只是一个简化.)
  31 #
  32 #  理论上来说, 如果打出的炮弹越多, 就越接近这个数字.
  33 #  然而, 对于shell 脚本来说一定会作些让步的,
  34 #+ 因为它肯定不能和那些内建就支持浮点运算的编译语言相比.
  35 #  当然就会降低精度.
  36 
  37 
  38 DIMENSION=10000  # 这块土地的边长.
  39                  # 这也是所产生的随机整数的上限.
  40 
  41 MAXSHOTS=1000    # 开炮次数.
  42                  # 10000 或更多次的话, 效果应该更好, 但有点太浪费时间了.
  43 PMULTIPLIER=4.0  # 接近于 PI 的比例因子.
  44 
  45 get_random ()
  46 {
  47 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
  48 RANDOM=$SEED                                  #  来自于 "seeding-random.sh"
  49                                               #+ 的例子脚本.
  50 let "rnum = $RANDOM % $DIMENSION"             #  范围小于 10000.
  51 echo $rnum
  52 }
  53 
  54 distance=        # 声明全局变量.
  55 hypotenuse ()    # 从 "alt-bc.sh" 例子来的,
  56 {                # 计算直角三角形的斜边的函数.
  57 distance=$(bc -l << EOF
  58 scale = 0
  59 sqrt ( $1 * $1 + $2 * $2 )
  60 EOF
  61 )
  62 #  设置 "scale" 为 0 , 好让结果四舍五入为整数值,
  63 #+ 这是这个脚本中必须折中的一个地方.
  64 #  不幸的是, 这将降低模拟的精度.
  65 }
  66 
  67 
  68 # main() {
  69 
  70 # 初始化变量.
  71 shots=0
  72 splashes=0
  73 thuds=0
  74 Pi=0
  75 
  76 while [ "$shots" -lt  "$MAXSHOTS" ]           # 主循环.
  77 do
  78 
  79   xCoord=$(get_random)                        # 取得随机的 X 与 Y 坐标.
  80   yCoord=$(get_random)
  81   hypotenuse $xCoord $yCoord                  #  直角三角形斜边 =
  82                                               #+ distance.
  83   ((shots++))
  84 
  85   printf "#%4d   " $shots
  86   printf "Xc = %4d  " $xCoord
  87   printf "Yc = %4d  " $yCoord
  88   printf "Distance = %5d  " $distance         #  到湖中心的
  89                                               #+ 距离 --
  90                                               #  起始坐标点 --
  91                                               #+  (0,0).
  92 
  93   if [ "$distance" -le "$DIMENSION" ]
  94   then
  95     echo -n "SPLASH!  "
  96     ((splashes++))
  97   else
  98     echo -n "THUD!    "
  99     ((thuds++))
 100   fi
 101 
 102   Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc)
 103   # 将比例乘以 4.0.
 104   echo -n "PI ~ $Pi"
 105   echo
 106 
 107 done
 108 
 109 echo
 110 echo "After $shots shots, PI looks like approximately $Pi."
 111 # 如果不太准的话, 那么就提高一下运行的次数. . .
 112 # 可能是由于运行错误和随机数随机程度不高造成的.
 113 echo
 114 
 115 # }
 116 
 117 exit 0
 118 
 119 #  要想知道一个shell脚本到底适不适合作为
 120 #+ 一种需要对复杂和精度都有要求的计算应用的模拟的话.
 121 #
 122 #  一般至少需要两个判断条件.
 123 #  1) 作为一种概念的验证: 来显示它可以做到.
 124 #  2) 在使用真正的编译语言来实现一个算法之前,
 125 #+    使用脚本来测试和验证这个算法.

dc

dc (桌面计算器desk calculator) 工具是面向栈的并且使用 RPN (逆波兰表达式 "Reverse Polish Notation" 又叫"后缀表达式"). 与 bc 命令很相像 , 但是这个工具具备好多只有编程语言才具备的能力.(正常表达式 逆波兰表达式 a+b a,b,+ a+(b-c) a,b,c,-,+ a+(b-c)*d a,d,b,c,-,*,+)

绝大多数人都避免使用这个工具, 因为它需要非直觉的 RPN 输入. 但是, 它却有特定的用途.


Example 12-46. 将10进制数字转换为16进制数字

   1 #!/bin/bash
   2 # hexconvert.sh: 将10进制数字转换为16进制数字
   3 
   4 E_NOARGS=65 # 缺命令行参数错误.
   5 BASE=16     # 16进制.
   6 
   7 if [ -z "$1" ]
   8 then
   9   echo "Usage: $0 number"
  10   exit $E_NOARGS
  11   # 需要一个命令行参数.
  12 fi
  13 # 练习: 添加命令行参数检查.
  14 
  15 
  16 hexcvt ()
  17 {
  18 if [ -z "$1" ]
  19 then
  20   echo 0
  21   return    # 如果没有参数传递到这个函数中就 "return" 0.
  22 fi
  23 
  24 echo ""$1" "$BASE" o p" | dc
  25 #                 "o" 设置输出的基数(数制).
  26 #                   "p" 打印栈顶.
  27 # 察看 dc 的 man 页来了解其他的选项.
  28 return
  29 }
  30 
  31 hexcvt "$1"
  32 
  33 exit 0

通过仔细学习 dc 命令的 info 页, 可以更深入的理解这个复杂的命令. 但是, 有一些精通 dc巫术 的小组经常会炫耀他们使用这个强大而又晦涩难懂的工具时的一些技巧, 并以此为乐.

 bash$ echo "16i[q]sa[ln0=aln100%Pln100/snlbx]sbA0D68736142snlbxq" | dc"
 Bash
 	      


Example 12-47. 因子分解

   1 #!/bin/bash
   2 # factr.sh: 分解约数
   3 
   4 MIN=2       # 如果比这个数小就不行了.
   5 E_NOARGS=65
   6 E_TOOSMALL=66
   7 
   8 if [ -z $1 ]
   9 then
  10   echo "Usage: $0 number"
  11   exit $E_NOARGS
  12 fi
  13 
  14 if [ "$1" -lt "$MIN" ]
  15 then
  16   echo "Number to factor must be $MIN or greater."
  17   exit $E_TOOSMALL
  18 fi  
  19 
  20 # 练习: 添加类型检查 (防止非整型的参数).
  21 
  22 echo "Factors of $1:"
  23 # ---------------------------------------------------------------------------------
  24 echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
  25 # ---------------------------------------------------------------------------------
  26 # 上边这行代码是 Michel Charpentier 编写的<charpov@cs.unh.edu>.
  27 # 在此使用经过授权 (thanks).
  28 
  29  exit 0

awk

在脚本中使用浮点运算的另一种方法是使用 awk 内建的数学运算函数, 可以用在shell wrapper中.


Example 12-48. 计算直角三角形的斜边

   1 #!/bin/bash
   2 # hypotenuse.sh: 返回直角三角形的斜边.
   3 #               ( 直角边长的平方和,然后对和取平方根)
   4 
   5 ARGS=2                # 需要将2个直角边作为参数传递进来.
   6 E_BADARGS=65          # 错误的参数值.
   7 
   8 if [ $# -ne "$ARGS" ] # 测试传递到脚本中的参数值.
   9 then
  10   echo "Usage: `basename $0` side_1 side_2"
  11   exit $E_BADARGS
  12 fi
  13 
  14 
  15 AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
  16 #              命令 / 传递给awk的参数
  17 
  18 
  19 # 现在, 将参数通过管道传递给awk.
  20 echo -n "Hypotenuse of $1 and $2 = "
  21 echo $1 $2 | awk "$AWKSCRIPT"
  22 
  23 exit 0