by falcon [email protected] of TinyLab.org
2007-10-30
最初發表:泰曉科技 – 聚焦嵌入式 Linux,追本溯源,見微知著!
原文鏈接:Shell編程範例之數值運算
評論說明:爲更好地聚合大家的討論,請到上面原文的評論區回覆。
前言
從本文開始,打算結合自己平時的積累和進一步的實踐,通過一些範例來介紹Shell編程。因爲範例往往能夠給人以學有所用的感覺,而且給人以動手實踐的機會,從而激發人的學習熱情。
考慮到易讀性,這裏的範例將非常簡單,但是實用,希望它們能夠成爲你解決常規問題的參照物或者是“茶餘飯後”的小點心,當然這些“點心”肯定還有值得探討、優化的地方。
更復雜有趣的例子請參考Advanced Bash-Scripting Guide(一本深入學習shell腳本藝術的書籍)。
該序列概要:
- 目的:享受用shell解決問題的樂趣;和朋友們一起交流和探討
- 計劃:先零散地寫些東西,之後再不斷補充,最後整理成冊。
- 適合讀者:已經熟悉linux基本知識,比如文件系統結構、常用命令行工具、shell編程基礎等。
- 建議:大家在看這些範例的時候,參考網絡中流傳的《shell基礎十二篇》和《shell十三問》,見ChinaUnix Shell討論區。
- 環境:如果沒有特別說明,以後《shell編程範例》系列使用的shell將特指bash,版本在3.1.17以上。
- 說明:本序列的組織不是依據Shell語法,而是面向某些潛在的操作對象和操作本身,它們反應了現實應用。當然,在這個過程當中肯定會涉及到Shell的語法。另外,歡迎您對帖子裏頭存在的問題進行批評指正,也歡迎您對一些範例進行改進。
正文
這一篇打算討論一下Shell編程中的基本數值運算,這類運算包括:
- 數值(包括整數和浮點數)間的加、減、乘、除、求冪、求模等
- 產生指定範圍的隨機數
- 產生指定範圍的數列
貌似Shell本身(注:Shell本身是一個解釋程序,你可以在命令行打印SHELL變量找到當前的shell程序)只可以完成整數運算,一些複雜的運算可以通過外部命令實現,比如expr,bc,awk等。至於隨機數,shell可以通過RANDOM環境變量產生一個從0到32767的隨機數,一些外部工具,比如awk可以通過rand()函數產生隨機數。而seq命令可以用來產生一個數列。下面對它們分別進行介紹。
整數運算
範例:對某個數加1
$ 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和i和1,即從左往右的第1個和第2個數。
用shell的內置命令查看各個命令的類型如下:
$ 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加到某個數
#!/bin/bash # calc.sh i=0; while [ $i -lt 10000 ] do ((i++)) done echo $i
說明:這裏通過while [ 條件表達式 ]; do … done循環來實現。-lt是小於號(<),具體見test命令的用法:man test。
如何執行該腳本?
第一種辦法直接把腳本文件當成子Shell(bash)的一個參數傳入。
$ bash calc.sh $ type bash bash is hashed (/bin/bash)
第二種辦法是通過bash的內置命令.或source執行。
$ . ./calc.sh
或
$ source ./calc.sh $ type . . is a shell builtin $ type source source is a shell builtin
第三種辦法是修改文件爲可執行,直接在當前shell下執行。
$ 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;}')
比較計算時間如下:
$ 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可以用來求冪,運算符不一樣,前者是**,後者是^。例如:
範例:求模5%2
$ expr 5 % 2 1 $ let i=5%2 $ echo $i 1 $ echo 5 % 2 | bc 1 $ ((i=5%2)) $ echo $i 1
範例:求冪5**2
$ let i=5**2 $ echo $i 25 $ ((i=5**2)) $ echo $i 25 $ echo "5^2" | bc 25
範例:8進制轉10進制
進制轉換也是比較常用的操作,可以用Bash的內置支持也可以用bc來完成,例如把8進制的11轉換爲10進制,則可以:
$ echo "obase=10;ibase=8;11" | bc -l 9 $ echo $((8#11)) 9
上面都是把某個進制的數轉換爲10進制的,如果要進行任意進制之間的轉換還是bc比較靈活,因爲它可以直接指定進制源和進制轉換目標。
範例:ascii字符編碼
如果要把某些字符串以特定的進製表示,可以用od命令,例如默認的分隔符IFS包括空格、TAB以及換行,可以用man ascii佐證。
$ echo -n "$IFS" | od -c 0000000 t n 0000003 $ echo -n "$IFS" | od -b 0000000 040 011 012 0000003
浮點運算
let和expr都無法進行浮點運算,但是bc和awk可以。
範例:求1除以13,保留3位有效數字。
$ echo "scale=3; 1/13" | bc .076 $ echo "1 13" | awk '{printf("%0.3fn",$1/$2)}' 0.077
說明:bc在進行浮點運算的時候需要指定小數點位數,否則默認爲0,即進行浮點運算的時候,默認求出的結果只保留整數。而awk在控制小數位數的時候非常靈活,僅僅通過printf的格式控制就可以實現。
補充:在用bc進行運算的時候,如果不指定scale,而在bc後加上-l選項,也可以進行浮點運算,只不過這時的浮點運算的小數點默認是20位。例如:
$ echo 1/13100 | bc -l .00007633587786259541
範例:餘弦值轉角度
用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("%s\n", atan2(sqrt(1-$1^2),$1)*180/3.1415926535);}' 4.93495
範例:數據分析
有一組數據,存有某村所有家庭的人數和月總收入,找出人均月收入最高家庭
在這裏隨機產生了一組測試數據,文件名爲income.txt。
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
說明:上面的三列數據分別是家庭編號、家庭人數、家庭月總收入。
分析:爲了求出月均收入最高的家庭,我們需要對後面兩列數進行除法運算,即求出每個家庭的月均收入,然後按照月均收入排序,找出收入最高的家庭。
實現:
#!/bin/bash # gettopfamily.sh [ $# -lt 1 ] && echo "please input the income file" && exit -1 [ ! -f $1 ] && echo "$1 is not a file" && exit -1 income=$1 awk '{ printf("%d %0.2fn", $1, $3/$2); }' $income | sort -k 2 -n -r
說明:
- [ $# -lt 1 ]:要求用戶至少收入一個參數,$#是shell中傳入參數的個數
- [ ! -f $1 ]:要求用戶傳入的參數是一個文件,-f的用法見test命令,man test
- income=$1:把用戶傳入的參數賦值給income變量,並在後面作爲awk的參數,即需要處理的文件
- awk:用文件中的第三列除以第二列,求出月均收入,考慮到精確性,保留了兩位有效數字。
- sort -k 2 -n -r:這裏對結果的awk結果的第二列(-k 2),即月均收入進行排序,按照數字排序(-n),並按照遞減的順序排序(-r)。
演示:
$ ./gettopfamily.sh income.txt 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.txt數據是隨機產生的。在做一些實驗時,往往需要隨機產生一些數據,在下一小節,我們將詳細介紹它。這裏是產生income.txt數據的腳本:
#!/bin/bash # genrandomdata.sh for i in $(seq 1 10) do echo $i $(($RANDOM/8192+3)) $((RANDOM/10+3000)) done
說明:上述腳本中還用到seq命令產生從1到10的一列數,這個命令的詳細用法在該篇最後一節也會進一步介紹。
隨機數
環境變量RANDOM產生0到32767的隨機數,而awk的rand函數可以產生0到1之間的隨機數。
範例:獲取一個隨機數
$ echo $RANDOM 81 $ echo "" | awk '{srand(); printf("%f", rand());}' 0.237788
說明:srand在無參數時,採用當前時間作爲rand隨機數產生器的一個seed。
範例:隨機產生一個從0到255之間的數字
可以通過RANDOM變量的縮放和awk中rand的放大來實現。
$ expr $RANDOM / 128 $ echo "" | awk '{srand(); printf("%d\n", rand()*255);}'
思考:如果要隨機產生某個IP段的IP地址,該如何做呢?看例子:友善地獲取一個可用的IP地址。
#!/bin/bash # getip.sh -- get an usable ipaddress automatically # author: falcon <[email protected]> # update: Tue Oct 30 23:46:17 CST 2007 # set your own network, default gateway, and the time out of "ping" command net="192.168.1" default_gateway="192.168.1.1" over_time=2 # check the current ipaddress ping -c 1 $default_gateway -W $over_time [ $? -eq 0 ] && echo "the current ipaddress is okey!" && exit -1; while :; do # clear the current configuration ifconfig eth0 down # configure the ip address of the eth0 ifconfig eth0 $net.$(($RANDOM /130 +2)) up # configure the default gateway route add default gw $default_gateway # check the new configuration ping -c 1 $default_gw -W $over_time # if work, finish [ $? -eq 0 ] && break done
說明:如果網關地址不是1,那麼用ifconfig配置地址時不能配置爲網關地址,否則你的IP地址將和網關一樣,導致整個網絡出現問題。
其他
其實通過一個循環就可以產生一序列數,但是有相關的小工具爲什麼不用呢!seq就是這麼一個小工具,它可以產生一序列數,你可以指定數的遞增間隔,也可以指定相鄰兩個數之間的分割符。
範例:獲取一序列數
$ 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 $ seq -f "0x%g" 1 5 0x1 0x2 0x3 0x4 0x5
一個比較典型的使用seq的例子,構造一些特定格式的鏈接,然後用wget下載這些內容:
$ for i in `seq -f"http://thns.tsinghua.edu.cn/thnsebooks/ebook73/%02g.pdf" 1 21`;do wget -c $i; done
或者
$ for i in `seq -w 1 21`;do wget -c "http://thns.tsinghua.edu.cn/thnsebooks/ebook73/$i"; done
補充:在bash版本3中,在for循環的in後面,可以直接通過{1…5}更簡潔地產生自1到5的數字(注意,1和5之間只有兩個點),例如:
$ for i in {1..5}; do echo -n "$i "; done 1 2 3 4 5
範例:統計字符串中各單詞出現次數
首先,我們統計某個文件中所有單詞的個數。
這裏的單詞我定義爲:由字母組成的單個或者多個字符序列。所以,可以這樣實現。
說明:爲了方便演示,這裏採用。
統計每個單詞出現的次數:
$ wget -c http://tinylab.org $ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c
統計出現頻率最高的前10個單詞:
$ wget -c http://tinylab.org $ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c | sort -n -k 1 -r | head -10 524 a 238 tag 205 href 201 class 193 http 189 org 175 tinylab 174 www 146 div 128 title
說明:
- cat index.html: 輸出index.html文件裏的內容
- sed -e “s/[^a-zA-Z]/\n/g”: 把非字母的字符全部替換成空格,這樣整個文本只剩下字母字符
- grep -v ^$:去掉空行
- sort: 排序
- uniq -c:統計相同行的個數,即每個單詞的個數
- sort -n -k 1 -r:按照第一列(-k 1)的數字(-n)逆序(-r)排序
- head -10:取出前十行
範例:統計指定單詞出現次數
可以考慮採取兩種辦法:
- 只統計那些需要統計的單詞
- 用上面的算法把所有單詞的個數都統計出來,然後再返回那些需要統計的單詞給用戶
不過,這兩種辦法都可以通過下面的結構來實現。
#!/bin/bash # statistic_words.sh if [ $# -lt 1 ]; then echo "ERROR: you should input 2 words at least"; echo "Usage: basename $0 FILE WORDS ...." exit -1 fi FILE=$1 ((WORDS_NUM=$#-1)) for n in $(seq $WORDS_NUM) do shift cat $FILE | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep ^$1$ | uniq -c done
說明:
- if 條件部分:要求用戶輸入至少兩個參數,第一個是需要統計單詞的文件名,第二之後的所有參數是需要統計的單詞。
- FILE=$1: 獲取文件名,即腳本之後的第一個字符串。
- ((WORDS_NUM=$#-1)):獲取單詞個數,即總的參數個數($#)減去那個文件名參數(1個)
- for循環部分:首先通過seq產生需要統計的單詞個數序列,shift是shell內置變量(請通過help shift獲取幫助),它把用戶從命令行中傳入的參數依次往後移動位置,並把當前參數作爲第一個參數即$1,這樣通過$1就可以遍歷用戶所有輸入的單詞(仔細一想,這裏貌似有數組下標的味道)。你可以考慮把shift之後的那句替換成echo $1測試shift的用法。
演示:
$ chmod +x statistic_words.sh $ ./statistic_words.sh index.html tinylab linux python 175 tinylab 43 linux 3 python
採用第二種辦法,我們只需要修改shift之後的那句即可。
#!/bin/bash # statistic_words.sh if [ $# -lt 1 ]; then echo "ERROR: you should input 2 words at least"; echo "Usage: basename $0 FILE WORDS ...." exit -1 fi FILE=$1 ((WORDS_NUM=$#-1)) for n in $(seq $WORDS_NUM) do shift cat $FILE | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c | grep " $1$" done
演示:
$ ./statistic_words.sh index.html tinylab linux python 175 tinylab 43 linux 3 python
說明:很明顯,採用第一種辦法效率要高很多,因爲第一種辦法提前找出了需要統計的單詞,然後再統計,而後者則不然。實際上,如果使用grep的-E選項,我們無須引入循環,而用一條命令就可以搞定:
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep -E "^tinylab$|^linux$" | uniq -c 43 linux 175 tinylab
或者
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | egrep "^tinylab$|^linux$" | uniq -c 43 linux 175 tinylab
說明:需要注意到sed命令可以直接處理文件,而無需通過cat命令輸出以後再通過管道傳遞,這樣可以減少一個不必要的管道操作,所以上述命令可以簡化爲:
$ sed -e "s/[^a-zA-Z]/\n/g" index.html | grep -v ^$ | sort | egrep "^tinylab$|^linux$" | uniq -c 43 linux 175 tinylab
所以,可見這些命令sed,grep,uniq,sort是多麼有用,它們本身雖然只完成簡單的功能,但是通過一定的組合,就可以實現你想要實現的功能啦。對了,統計單詞還有個非常有用的命令wc -w,需要用到的時候也可以用它。
補充:在Advanced Bash-Scripting Guide一書中還提到jot命令和factor命令,由於機器上沒有,所以沒有測試,factor命令可以產生某個數的所有素數。如:
$ factor 100 100: 2 2 5 5
總結
到這裏,shell編程範例之數值計算就結束啦。該篇主要介紹了:
- shell編程中的整數運算、浮點運算、隨機數的產生、數列的產生
- shell的內置命令、外部命令的區別,以及如何查看他們的類型和幫助,關於內置命令和外部命令的比較
- shell腳本的幾種執行辦法
- 幾個常用的shell外部命令:sed,awk,grep,uniq,sort等
- 範例:數字遞增;求月均收入;自動獲取IP地址;統計單詞個數
- 其他:相關的用法,比如命令列表,條件測試等,在上述範例中都已經涉及,請認真閱讀之
如果您有時間,請溫習之。
資料
- Advanced Bash-Scripting Guide
- shell十三問
- shell基礎十二篇
- SED手冊
- AWK使用手冊
- 幾個shell討論區
- LinuxSir.org
- ChinaUnix.net
後記
大概花了3個多小時才寫完,目前是23:33,該回宿舍睡覺啦,明天起來修改錯別字和補充一些內容,朋友們晚安!
10月31號,修改部分措辭,增加一篇統計家庭月均收入的範例,添加總結和參考資料,並用附錄所有代碼。
SHELL編程是一件非常有趣的事情,如果您想一想:上面計算家庭月均收入的例子,然後和用M$ Excel來做這個工作比較,你會發現前者是那麼簡單和省事,而且給您以運用自如的感覺。