| Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting | ||
|---|---|---|
| Prev | Chapter 12. External Filters, Programs and Commands | Next | 
将一个正数分解为多个素数.
| bash$ factor 27417 27417: 3 13 19 37 | 
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. 计算圆周率
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 内建的数学运算函数, 可以用在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 |