兴海北路

---男儿仗剑自横行
<2008年3月>
2425262728291
2345678
9101112131415
16171819202122
23242526272829
303112345

统计

  • 随笔 - 85
  • 文章 - 0
  • 评论 - 17
  • 引用 - 0

常用链接

留言簿(6)

随笔分类

随笔档案

收藏夹

全是知识啊

搜索

  •  

最新评论

阅读排行榜

评论排行榜

shell编程范例之数值运算

by falcon<zhangjinw@gmail.com>
2007-10-30

前言:
   
    从这个帖子开始,打算结合自己平时的积累和进一步的实践,通过一些范例来介绍shell编程。因为范例往往能够给人以学有所用的感觉,而且给人以动手实践 的机会,从而激发人的学习热情。考虑到易读性,这里的范例将非常简单,但是实用,希望它们能够成为你解决常规问题的参照物或者是“茶余饭后”的小点心,当 然这些“点心”肯定还有值得探讨、优化的地方。更复杂有趣的例子请参考《高级Bash脚本编程指南》(一本深入学习shell脚本艺术的书籍)。

    该计划是上一篇帖子《在Linux下更高效的工作》的续。

    写这些东西的

Quote:

    目的:1)享受用shell解决问题的乐趣 2)和朋友们一起交流和探讨
    初步计划:先零散地写些东西,之后再不断补充,最后整理成册。
    适合读者:已经熟悉linux基本知识,比如文件系统结构、常用命令行工具、shell编程基础等。
    建议:大家在看这些范例的时候,参考网络中流传的《shell基础十二篇》和《shell十三问》,见ChinaUnix Shell讨论区
    环境:如果没有特别说明,以后"shell编程范例"系列使用的shell将特指bash,版本在3.1.17以上。
    说明:本序列的组织不是依据Shell语法,而是面向某些潜在的操作对象和操作本身,它们反应了现实应用。当然,在这个过程当中肯定会涉及到Shell的语法。另外,欢迎您对帖子里头存在的问题进行批评指正,也欢迎您对一些范例进行改进。


shell编程范例之数值运算

    这一篇打算讨论一下shell编程中的基本数值运算,这类运算包括数值(包括整数和浮点数)间的加减乘除幂求模等,产生指定范围的随机数,产生指定范围的数列等。
    貌似shell本身(shell本身是一个解释程序,你可以在命令行打印SHELL变量找到当前的shell程序)只可以完成整数运算,一些复杂的运算可 以通过外部命令实现,比如expr,bc,awk等。至于随机数,shell可以通过RANDOM环境变量产生一个从0到32767的随机数,一些外部工 具,比如awk可以通过rand()函数产生随机数。而seq命令可以用来产生一个数列。
    下面分别进行介绍:

1、整数运算

1.1 概要示例:对某个数加一

Quote:

$ i=0;
$ ((i++))
$ echo $i
1
$ let i++
$ echo $i
2
$ expr $i + 1
3
$ echo $i
2
$ echo $i 1 | awk '{printf $1+$2}'
3



    说明:expr之后的$i,+,1之间有空格分开;awk后面的$1和$2分别指$i和1,即从左往右的第1个和第二个数。

用shell的内置命令查看各个命令的类型如下:
Quote:

$ type type
type is a shell builtin
$ type let
let is a shell builtin
$ type expr
expr is hashed (/usr/bin/expr)
$ type bc
bc is hashed (/usr/bin/bc)
$ type awk
awk is /usr/bin/awk



    从上面的演示可以看出:let是shell内置命令,其他几个是外部命令,都在/usr/bin目录下。而expr和bc因为我刚用过,已经加载在内存的hash表中。这个结果将有助于我们理解下面范例的结果。

补充:如果查看不同命令的帮助
    对于let和type等shell内置命令,可以通过shell的一个内置命令help来查看相关帮助,而一些外部命令可以通过shell的一个外部命令man来查看帮助,用法诸如help let,man expr等。

1.2 范例演示:从1加到某个数值。

代码:


Code:

[Ctrl+A Select All]



说明:这里通过while [ 条件表达式 ]; do .... done循环来实现。-lt是小于号(<),具体见test命令的用法:man test。

如何执行该脚本?

第一种办法直接把脚本文件当成子shell(bash)的一个参数传入。
Quote:

$ bash calc.sh
$ type bash
bash is hashed (/bin/bash)



第二种办法是通过bash的内置命令.或source执行。
Quote:

$ . ./calc.sh

$ source ./calc.sh
$ type .
. is a shell builtin
$ type source
source is a shell builtin



第三种办法是修改文件为可执行,直接在当前shell下执行。
Quote:

$ chmod ./calc.sh
$ ./calc.sh



下面,逐一演示用其他方法计算变量加一,即把((i++))行替换成下面的某一个:
let i++;
i=$(expr $i + 1)
i=$(echo $i+1|bc)
i=$(echo "$i 1" | awk '{printf $1+$2;}')

比较计算时间如下:
Quote:

$ time calc.sh
10000

real    0m1.319s
user    0m1.056s
sys     0m0.036s
$ time calc_let.sh
10000

real    0m1.426s
user    0m1.176s
sys     0m0.032s
$  time calc_expr.sh
1000

real    0m27.425s
user    0m5.060s
sys     0m14.177s
$ time calc_bc.sh
1000

real    0m56.576s
user    0m9.353s
sys     0m24.618s
$ time ./calc_awk.sh
100

real    0m11.672s
user    0m2.604s
sys     0m2.660s



说明:time命令可以用来统计命令执行时间,这部分时间包括总的运行时间,用户空间执行时间,内核空间执行时间,它通过ptrace系统调用实现。

总 结:通过上面的比较,我们发现(())的运算效率最高。而let作为shell内置命令,效率也很高,但是expr,bc,awk的计算效率就比较低。所 以,在shell本身能够完成相关工作的情况下,建议优先使用shell本身提供的功能。但是shell本身好像无法完成浮点运算,所以就需要外部命令的 帮助。

补充:let,expr,bc都可以用来求模,运算符都是%,而let和bc可以用来求幂,运算符不一样,前者是**,后者是^。例如:

Quote:

//求模
$ expr 5 % 2
1
$ let i=5%2
$ echo $i
1
$ echo 5 % 2 | bc
1
$ ((i=5%2))
$ echo $i
1
//求幂
$ let i=5**2
$ echo $i
25
$ ((i=5**2))
$ echo $i
25
$ echo "5^2" | bc
25



2. 浮点运算

let和expr都无法进行浮点运算,但是bc和awk可以。

2.1 概要示例:求1除以13,保留3位有效数字。

Quote:

$ echo "scale=3; 1/13"  | bc
.076
$ echo "1 13" | awk '{printf("%0.3f\n",$1/$2)}'
0.077



说明:bc在进行浮点运算的时候需要指定小数点位数,否则默认为0,即进行浮点运算的时候,默认求出的结果只保留整数。而awk在控制小数位数的时候非常灵活,仅仅通过printf的格式控制就可以实现。

补充:在用bc进行运算的时候,如果不指定scale,而在bc后加上-l选项,也可以进行浮点运算,只不过这时的浮点运算的小数点默认是20位。例如:

Quote:

$ echo 1/13100 | bc -l
.00007633587786259541



2.2 范例演示:假如有这样一组数据,存放有某个村庄所有家庭的人数和月总收入,要求找出人均月收入最高的家庭。

在这里我随机产生了一组数据,文件名为income。
Quote:

1 3 4490
2 5 3896
3 4 3112
4 4 4716
5 4 4578
6 6 5399
7 3 5089
8 6 3029
9 4 6195
10 5 5145



说明:上面的三列数据分别是家庭编号、家庭人数、家庭月总收入。
分析:为了求出月均收入最高的家庭,我们需要对后面两列数进行除法运算,即求出每个家庭的月均收入,然后按照月均收入排序,找出收入最高的家庭。
实现:



Code:

[Ctrl+A Select All]



说明:
[ $# -lt 1 ]: 要求用户至少收入一个参数,$#是shell中传入参数的个数
[ ! -f $1 ] : 要求用户传入的参数是一个文件,-f的用法见test命令,man test
income=$1:把用户传入的参数赋值给income变量,并在后面作为awk的参数,即需要处理的文件
awk....:用文件中的第三列除以第二列,求出月均收入,考虑到精确性,保留了两位有效数字。
sort -k 2 -n -r: 这里对结果的awk结果的第二列(-k 2),即月均收入进行排序,按照数字排序(-n),并按照递减的顺序排序(-r)。

演示:
Quote:

$ ./gettopfamily.sh income
7 1696.33
9 1548.75
1 1496.67
4 1179.00
5 1144.50
10 1029.00
6 899.83
2 779.20
3 778.00
8 504.83



补充:之前的income数据是随机产生的。在做一些实验时,往往需要随机产生一些数据,在下一小节,我们将详细介绍它。这里是产生income数据的脚本:



Code:

[Ctrl+A Select All]



说明:上述脚本中还用到seq命令产生从1到10的一列数,这个命令的详细用法在该篇最后一节也会进一步介绍。

3. 随机数

环境变量RANDOM产生0到32767的随机数,而awk的rand函数可以产生0到1之间的随机数。

3.1 概要示例:打印一个随机数

Quote:

$ echo $RANDOM
81
$ echo "" | awk '{srand(); printf("%f", rand());}'
0.237788



说明:srand在无参数时,采用当前时间作为rand随机数产生器的一个seed。

3.2 范例演示:随机产生一个从0到255之间的数字

3.2.1 可以通过RANDOM变量的缩放和awk中rand的放大来实现。

Quote:

$ expr $RANDOM / 128
$ echo "" | awk '{srand(); printf("%d\n", rand()*255);}'



思考:如果要随机产生某个IP段的IP地址,该如何做呢?

3.2.2 友善地获取一个可用的IP地址

这个脚本我在兰大开源社区的讨论区发过,具体的分析过程见《貌似IP地址老被抢,写个脚本自动换个可用的(非破坏性)

代码:


Code:

[Ctrl+A Select All]



说明:如果网关地址不是1,那么用ifconfig配置地址时不能配置为网关地址,否则你的IP地址将和网关一样,导致整个网络出现问题。

4. 产生一序列数

其实我们通过一个循环就可以产生一序列数,但是有相关的小工具为什么不用呢!seq就是这么一个小工具,它可以产生一序列数,你可以指定数的递增间隔,也可以指定相邻两个数之间的分割符。

4.1 概要示例:演示seq,打印一序列数

Quote:

$ seq 5
1
2
3
4
5
$ seq 1 5
1
2
3
4
5
$ seq 1 2 5
1
3
5
$ seq -s: 1 2 5
1:3:5
$ seq 1 2 14
1
3
5
7
9
11
13
$ seq -w 1 2 14
01
03
05
07
09
11
13
$ $ seq -s: -w 1 2 14
01:03:05:07:09:11:13



补充:在bash版本3中,在for循环的in后面,可以直接通过{1..5}更简洁地产生自1到5的数字(注意,1和5之间只有两个点),例如:
Quote:

$ for i in {1..5}; do echo -n "$i "; done
1 2 3 4 5

4.2 统计指定字符串(这里以单词为例)的个数

这个灵感来自《高级Bash脚本编程指南》“混杂命令”seq的实例之“字母统计”和CU上一篇统计字母和数字个数的帖子。

4.2.1 首先,我们统计某个文件中所有单词的个数。这里的单词我定义为:由字母组成的单个或者多个字符序列。所以,可以这样实现。

说明:为了方便演示,这里采用我的上一篇转载的日志happiness quotations里头的内容,请把内容复制下来保存为text文件。

Quote:

//统计每个单词出现的次数
$ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c
//统计出出现频率最高的前10个单词
$ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c | sort -n -k 1 -r | head -10
     45 is
     40 to
     32 of
     31 a
     28 and
     27 the
     25 happiness
     19 happy
     18 it
     18 in



说明:
cat text: 显示text文件里的内容
sed -e "s/[^a-zA-Z]/\n/g": 把非字母的字符全部替换成空格,这样整个文本只剩下字母字符
grep -v ^$:去掉空行
sort: 排序
uniq -c:统计相同行的个数,即每个单词的个数
sort -n -k 1 -r:按照第一列(-k 1)的数字(-n)逆序(-r)排序
head -10:取出前十行

4.2.2 接着我们统计指定单词的个数,即输入需要统计的单词,并返回每个单词的个数。

可以考虑采取两种办法:
第一种:只统计那些需要统计的单词
第二种:用上面的算法把所有单词的个数都统计出来,然后再返回那些需要统计的单词给用户

不过,这两种办法都可以通过下面的结构来实现。



Code:

[Ctrl+A Select All]



说明:
if 条件部分:要求用户输入至少两个参数,第一个是需要统计单词的文件名,第二之后的所有参数是需要统计的单词。
FILE=$1: 获取文件名,即脚本之后的第一个字符串。
((WORDS_NUM=$#-1)):获取单词个数,即总的参数个数($#)减去那个文件名参数(1个)
for 循环部分:首先通过seq产生需要统计的单词个数序列,shift是shell内置变量(请通过help shift获取帮助),它把用户从命令行中传入的参数依次往后移动位置,并把当前参数作为第一个参数即$1,这样通过$1就可以遍历用户所有输入的单词 (仔细一想,这里貌似有数组下标的味道)。你可以考虑把shift之后的那句替换成echo $1测试shift的用法。

演示:
Quote:

$ chmod +x statistic_words.sh
$ ./statistic_words.sh text is Action happy
     45 is
      1 Action
     19 happy



采用第二种办法,我们只需要修改shift之后的那句即可。


Code:

[Ctrl+A Select All]



演示:
Quote:

$ ./statistic_words.sh text is Action happy
     45 is
      1 Action
     19 happy



说明:很明显,采用第一种办法效率要高很多,因为第一种办法提前找出了需要统计的单词,然后再统计,而后者则不然。实际上,如果使用grep的-E选项,我们无须引入循环,而用一条命令就可以搞定:

Quote:

$ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep -E "^Action$|^is$" | uniq
-c
或者
$ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | egrep  "^Action$|^is$" | uniq
-c
[/code]
所以,可见这些命令sed,grep,uniq,sort是多么有用,它们本身虽然只完成简单的功能,但是通过一定的组合,就可以实现你想要实现的功能啦。对了,统计单词还有个非常有用的命令wc -w,需要用到的时候也可以用它。

补充:在《高级Bash脚本编程指南》一书中还提到jot命令和factor命令,由于我机器上没有,所以没有测试,factor命令可以产生某个数的所有素数。如:
[quote]
$ factor 100
100: 2 2 5 5



5. 总结

到这里,shell编程范例之数值计算就结束啦。该篇主要介绍了:

* shell编程中的整数运算、浮点运算、随机数的产生、数列的产生
* shell的内置命令、外部命令的区别,以及如何查看他们的类型和帮助,关于内置命令和外部命令的比较,请参考:http://www.linuxpk.com/doc/abs/internal.html#READPIPEREF
* shell脚本的几种执行办法
* 几个常用的shell外部命令:sed,awk,grep,uniq,sort等
* 范例:数字递增;求月均收入;自动获取IP地址;统计单词个数
* 其他:相关的用法,比如命令列表,条件测试等,在上述范例中都已经涉及,请认真阅读之

如果您有时间,请温习之。

6. 参考资料和推荐资料

[1] 高级Bash脚本编程指南
http://www.linuxpk.com/doc/abs/
[2] shell十三问
http://bbs.chinaunix.net/thread-218853-1-1.html
[3] shell基础十二篇
http://bbs.chinaunix.net/thread-452942-1-1.html
[4] 在linux下学习和工作
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=775&forum=6
[5] 在linux下更高效的工作
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=1074&forum=6
[6] SED手册
http://phi.sinica.edu.tw/aspac/reports/96/96005/
[7] AWK使用手册
http://www.chinaunix.net/jh/7/16985.html
http://phi.sinica.edu.tw/aspac/reports/94/94011/
[8] 几个shell讨论区
兰大开源社区: http://oss.lzu.edu.cn/modules/newbb/viewforum.php?forum=26
LinuxSir.org: http://www.linuxsir.org/bbs/forumdisplay.php?f=60
ChinaUnix.net: http://bbs.chinaunix.net/forum-24-1.html

如果合适,建议直接找对应的英文原版阅读!

后记:

[1] 大概花了3个多小时才写完,目前是23:33,该回宿舍睡觉啦,明天起来修改错别字和补充一些内容,朋友们晚安!
[2] 10月31号,修改部分措辞,增加一篇统计家庭月均收入的范例,添加总结和参考资料,并用附录所有代码。
[3] SHELL编程是一件非常有趣的事情,如果您想一想:上面计算家庭月均收入的例子,然后和用M$ Excel来做这个工作比较,你会发现前者是那么简单和省事,而且给您以运用自如的感觉。



描述:shell_examples_calculate
附件: shell_examples_calculate.tar.gz (5 K)

再附一个简单范例(问题见XBW linux&unix版)

问题:有这么两个文件,第一列是坐标点,第二列是对应的值,要求把两文件中相同坐标处的值求和,结果格式和原文件一致。

分析:这个问题如果用shell做,用awk最合适不过,当然,还用到sort进行排序预处理。

$ cat A
0.000000 -393.339844
1.000000 -403.556091
2.000000 -408.335876
3.000000 -391.387726
4.000000 -406.563660
5.000000 -413.982544
$ cat B
0.000000 -20.100649
1.000000   -9.304893
2.000000   -7.830594
3.000000 -29.411428
4.000000   -9.393303
5.000000 -23.742157
$ sort A B | awk 'BEGIN{oldpoint=-1;}{ if(oldpoint==$1){ printf("%f %f\n", $1, $2+oldvalue); } oldpoint=$1; oldvalue=$2; }'
0.000000 -413.440493
1.000000 -412.860984
2.000000 -416.166470
3.000000 -420.799154
4.000000 -415.956963
5.000000 -437.724701
关于余弦值转角度

Quote:

//用bc -l计算,可以获得高精度
$ export cos=0.996293; echo "scale=100; a(sqrt(1-$cos^2)/$cos)*180/(a(1)*4)" |bc -l
4.934954755411383632719834036931840605159706398655243875372764917732\
5495504159766011527078286004072131
//用awk
$ echo 0.996293 | awk '{ printf("%-10s %-10s", $1, atan2(sqrt(1-$1^2),$1)*180/3.1415926535);}'



把一个文件中第2列的所有余弦值转换为角度

Quote:

$ cat data
2000     1         0
2005     0.996293     4.93515
2010     0.999609     1.60303
2015     0.98501     9.9332
2020     0.999488     1.83398
2025     0.999409     1.9702
2030     0.99943     1.93381
2035     0.999267     2.19348
2040     0.99679     4.59235
$ awk '{ printf("%-10s%-10s%-10s", $1, $2, $3); system("echo \"scale=10; a(sqrt(1-"$2"^2)/"$2")*180/3.1415926535\" | bc -l ");}' data
2000     1       0       0
2005     0.996293 4.93515   4.9349547602
2010     0.999609 1.60303   1.6022865295
2015     0.98501   9.9332   9.9330460660
2020     0.999488 1.83398   1.8335432105
2025     0.999409 1.9702   1.9699389820
2030     0.99943   1.93381   1.9346200645
2035     0.999267 2.19348   2.1938966333
2040     0.99679   4.59235   4.5920476729
$ awk '{ printf("%-10s%-10s%-10s %f\n", $1, $2, $3, atan2(sqrt(1-$2^2), $2)*180/3.1415926535)}' data
2000     1       0       0.000000
2005     0.996293 4.93515   4.934955
2010     0.999609 1.60303   1.602286
2015     0.98501   9.9332   9.933046
2020     0.999488 1.83398   1.833543
2025     0.999409 1.9702   1.969939
2030     0.99943   1.93381   1.934620
2035     0.999267 2.19348   2.193897
2040     0.99679   4.59235   4.592048



详细解答过程请参考:

http://bbs.lzu.edu.cn/wForum/disparticle.php?boardName=LinuxUnix&ID=28597&pos=2

感兴趣的朋友看看这些范例:
1. 《Shell 编程实例集锦》
http://www.lupaworld.com/35714/viewspace_21170.html

另外,通过这篇可以深入学习一下AWK的实际应用价值:
2. 巧用AWK处理二进制数据文件
http://www.ibm.com/developerworks/cn/linux/shell/awk/binary/

关于不同类型的数值常量(如八进制、16进制的表示等)的表示,请参考 《高级Bash脚本编程指南》http://www.linuxpk.com/doc/abs/numerical-constants.html#NUMBERS

东西总是学不完,大伙继续,这里是几个好去处,一天看上几篇,保证受益不少:

[1] linuxsir.org Shell版精华
http://www.linuxsir.org/bbs/forum60--1-desc-goodnees.html
[2] chinaunix.net Shell版综合水平测试
http://bbs.chinaunix.net/thread-476260-27-1.html
[3] linuxsir.org Shell技巧交流区
http://www.linuxsir.org/bbs/thread173263.html
[4] linuxsir.org Shell脚本欣赏区
http://www.linuxsir.org/bbs/showthread.php?threadid=29701

用bc计算器计算“Unix高级编程”第一章课后系统最后两个题目的计算过程:

Quote:

// 1.5 若日历存放在带符号的32位整数中,那么到哪一年它将溢出?
$ echo "(2^31)/(360*24*60*60)+1970-1" | bc
2038
// 1.6 若进程时间存放在带符号的32位整数中,而且每秒为100滴答,那么经过多少天后,该时间值将会溢出。
$ echo "2^31/(100*24*60*60)" | bc
248



以上两道题需要明白两个概念:
第一就是Unix时间存放的是从1970年1月1日到现在的秒数,第二格式进程时间存放的是进程运行到现在的滴答数。

如果有时间,这里头的一些资料还是值得您仔细阅读的:

"developerWorks 中国 | Shell、Shell 脚本编写、命令行、相关工具及技巧"
http://www.ibm.com/developerworks/cn/linux/shell/index.html
需要补充的是,日期和时间是一个很好的随机数,它是一个在永恒变化的东西,所以,你无须担心存在重复,它很适合用于生成一些临时文件。具体用法见man date,常用的有date +%s

刚从http://www.linuxpk.com/doc/abs/special-chars.html看到有趣的一点,用(())加#还可以转换进制。
例如:

Quote:

# echo "$(( 8#11 ))"
9



即1*8^0 + 1*8^1 = 9

关于间接变量的引用问题:

例如:

Quote:

$ a=b
$ b=c
$ echo $a
b
$ echo $b
c
$ eval echo \$$a
c
$ echo ${!a}
c



${!a}提供了一种非常方便的间接变量引用办法,参考:
http://www.linuxpk.com/doc/abs/othertypesv.html

posted on 2008-03-14 15:48 随意门 阅读(5297) 评论(2)  编辑 收藏 引用

评论

# re: shell编程范例之数值运算 2008-03-19 15:03 随意门

  回复  更多评论    

# re: shell编程范例之数值运算[未登录] 2009-01-09 10:05 Chris

楼主写得真的很好,像一份很好的教程。
  回复  更多评论    

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理