shell編程范例之數值運算
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