by falcon<zhangjinw@gmail.com>
2007-10-30
前言:
从这个帖子开始,打算结合自己平时的积累和进一步的实践,通过一些范例来介绍shell编程。因为范例往往能够给人以学有所用的感觉,而且给人以动手实践的机会,从而激发人的学习热情。考虑到易读性,这里的范例将非常简单,但是实用,希望它们能够成为你解决常规问题的参照物或者是“茶余饭后”的小点心,当然这些“点心”肯定还有值得探讨、优化的地方。更复杂有趣的例子请参考《高级Bash脚本编程指南》(一本深入学习shell脚本艺术的书籍)。
该计划是上一篇帖子《在Linux下更高效的工作》的续。
写这些东西的
Quote: |
目的:1)享受用shell解决问题的乐趣 2)和朋友们一起交流和探讨 |
shell编程范例之数值运算
这一篇打算讨论一下shell编程中的基本数值运算,这类运算包括数值(包括整数和浮点数)间的加减乘除幂求模等,产生指定范围的随机数,产生指定范围的数列等。
貌似shell本身(shell本身是一个解释程序,你可以在命令行打印SHELL变量找到当前的shell程序)只可以完成整数运算,一些复杂的运算可以通过外部命令实现,比如expr,bc,awk等。至于随机数,shell可以通过RANDOM环境变量产生一个从0到32767的随机数,一些外部工具,比如awk可以通过rand()函数产生随机数。而seq命令可以用来产生一个数列。
下面分别进行介绍:
1、整数运算
1.1 概要示例:对某个数加一
Quote: |
$ i=0; |
说明:expr之后的$i,+,1之间有空格分开;awk后面的$1和$2分别指$i和1,即从左往右的第1个和第二个数。
用shell的内置命令查看各个命令的类型如下:
Quote: |
$ type type |
从上面的演示可以看出: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 |
第二种办法是通过bash的内置命令.或source执行。
Quote: |
$ . ./calc.sh |
第三种办法是修改文件为可执行,直接在当前shell下执行。
Quote: |
$ chmod ./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 |
说明:time命令可以用来统计命令执行时间,这部分时间包括总的运行时间,用户空间执行时间,内核空间执行时间,它通过ptrace系统调用实现。
总结:通过上面的比较,我们发现(())的运算效率最高。而let作为shell内置命令,效率也很高,但是expr,bc,awk的计算效率就比较低。所以,在shell本身能够完成相关工作的情况下,建议优先使用shell本身提供的功能。但是shell本身好像无法完成浮点运算,所以就需要外部命令的帮助。
补充:let,expr,bc都可以用来求模,运算符都是%,而let和bc可以用来求幂,运算符不一样,前者是**,后者是^。例如:
Quote: |
//求模 |
2. 浮点运算
let和expr都无法进行浮点运算,但是bc和awk可以。
2.1 概要示例:求1除以13,保留3位有效数字。
Quote: |
$ echo "scale=3; 1/13" | bc |
说明:bc在进行浮点运算的时候需要指定小数点位数,否则默认为0,即进行浮点运算的时候,默认求出的结果只保留整数。而awk在控制小数位数的时候非常灵活,仅仅通过printf的格式控制就可以实现。
补充:在用bc进行运算的时候,如果不指定scale,而在bc后加上-l选项,也可以进行浮点运算,只不过这时的浮点运算的小数点默认是20位。例如:
Quote: |
$ echo 1/13100 | bc -l |
2.2 范例演示:假如有这样一组数据,存放有某个村庄所有家庭的人数和月总收入,要求找出人均月收入最高的家庭。
在这里我随机产生了一组数据,文件名为income。
Quote: |
1 3 4490 |
说明:上面的三列数据分别是家庭编号、家庭人数、家庭月总收入。
分析:为了求出月均收入最高的家庭,我们需要对后面两列数进行除法运算,即求出每个家庭的月均收入,然后按照月均收入排序,找出收入最高的家庭。
实现:
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 |
补充:之前的income数据是随机产生的。在做一些实验时,往往需要随机产生一些数据,在下一小节,我们将详细介绍它。这里是产生income数据的脚本:
Code:
[Ctrl+A Select All]
说明:上述脚本中还用到seq命令产生从1到10的一列数,这个命令的详细用法在该篇最后一节也会进一步介绍。
3. 随机数
环境变量RANDOM产生0到32767的随机数,而awk的rand函数可以产生0到1之间的随机数。
3.1 概要示例:打印一个随机数
Quote: |
$ echo $RANDOM |
说明:srand在无参数时,采用当前时间作为rand随机数产生器的一个seed。
3.2 范例演示:随机产生一个从0到255之间的数字
3.2.1 可以通过RANDOM变量的缩放和awk中rand的放大来实现。
Quote: |
$ expr $RANDOM / 128 |
思考:如果要随机产生某个IP段的IP地址,该如何做呢?
3.2.2 友善地获取一个可用的IP地址
这个脚本我在 兰大开源社区的讨论区发过,具体的分析过程见《 貌似IP地址老被抢,写个脚本自动换个可用的(非破坏性)》
代码:
Code:
[Ctrl+A Select All]
说明:如果网关地址不是1,那么用ifconfig配置地址时不能配置为网关地址,否则你的IP地址将和网关一样,导致整个网络出现问题。
4. 产生一序列数
其实我们通过一个循环就可以产生一序列数,但是有相关的小工具为什么不用呢!seq就是这么一个小工具,它可以产生一序列数,你可以指定数的递增间隔,也可以指定相邻两个数之间的分割符。
4.1 概要示例:演示seq,打印一序列数
Quote: |
$ seq 5 |
补充:在bash版本3中,在for循环的in后面,可以直接通过{1..5}更简洁地产生自1到5的数字(注意,1和5之间只有两个点),例如:
Quote: |
$ for i in {1..5}; do echo -n "$i "; done |
4.2 统计指定字符串(这里以单词为例)的个数
这个灵感来自《高级Bash脚本编程指南》“混杂命令”seq的实例之“字母统计”和CU上一篇统计字母和数字个数的帖子。
4.2.1 首先,我们统计某个文件中所有单词的个数。这里的单词我定义为:由字母组成的单个或者多个字符序列。所以,可以这样实现。
说明:为了方便演示,这里采用我的上一篇转载的日志happiness quotations里头的内容,请把内容复制下来保存为text文件。
Quote: |
//统计每个单词出现的次数 |
说明:
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 |
采用第二种办法,我们只需要修改shift之后的那句即可。
Code:
[Ctrl+A Select All]
演示:
Quote: |
$ ./statistic_words.sh text is Action happy |
说明:很明显,采用第一种办法效率要高很多,因为第一种办法提前找出了需要统计的单词,然后再统计,而后者则不然。实际上,如果使用grep的-E选项,我们无须引入循环,而用一条命令就可以搞定:
Quote: |
$ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep -E "^Action$|^is$" | uniq 补充:在《高级Bash脚本编程指南》一书中还提到jot命令和factor命令,由于我机器上没有,所以没有测试,factor命令可以产生某个数的所有素数。如: |
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做,用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计算,可以获得高精度 |
把一个文件中第2列的所有余弦值转换为角度
Quote: |
$ cat data |
详细解答过程请参考:
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/
[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
Quote: |
// 1.5 若日历存放在带符号的32位整数中,那么到哪一年它将溢出? |
以上两道题需要明白两个概念:
第一就是Unix时间存放的是从1970年1月1日到现在的秒数,第二格式进程时间存放的是进程运行到现在的滴答数。
"developerWorks 中国 | Shell、Shell 脚本编写、命令行、相关工具及技巧"
http://www.ibm.com/developerworks/cn/linux/shell/index.html
例如:
Quote: |
# echo "$(( 8#11 ))" |
即1*8^0 + 1*8^1 = 9
例如:
Quote: |
$ a=b |
${!a}提供了一种非常方便的间接变量引用办法,参考:
http://www.linuxpk.com/doc/abs/othertypesv.html