$ i=0;$ ((i++))$ echo $i1$ let i++$ echo $i2$ expr $i + 13$ echo $i2$ echo $i 1 | awk '{printf $1+$2}'3
說明:
expr之後的
$i,
+,1 之間有空格分開。如果進行乘法運算,需要對運算子進行轉義,否則Shell 會把乘號解釋為通配符,導致語法錯誤;
awk後面的
$1和
$2分別指
$i和1,即從左往右的第1 個和第2 個數。
用Shell 的內建指令查看各個指令的類型如下:
$ type typetype is a shell builtin$ type letlet is a shell builtin$ type exprexpr is hashed (/usr/bin/expr)$ typebc is hashed (/usr/bin/bc)$ type awkawk is /usr/bin/k
從上述示範可看出:
let是Shell 內建指令,其他幾個是外部指令,都在
/usr/bin目錄下。而
expr和
bc因為剛用過,已經載入在記憶體的
hash表中。這將有利於我們理解上一章介紹的腳本多種執行方法背後的原理。
說明:如果要查看不同命令的幫助,對於
let和
type等Shell 內建指令,可以透過Shell 的一個內建指令
help來查看相關幫助,而一些外部命令可以透過Shell 的一個外部命令
man來查看幫助,用法諸如
help let,
man expr等。
#!/bin/bash# calc.shi=0;while [ $i -lt 10000 ]do ((i++))doneecho $i
說明:這裡透過
while [ 條件式]; do .... done循環來實現。
-lt是小於號
<,具體見
test命令的用法:
man test。
如何執行該腳本?
辦法一:直接把腳本檔當成子Shell (Bash)的一個參數傳入
$ bash calc.sh$ type bashbash is hashed (/bin/bash)
辦法二:是透過
bash的內建指令
.或
source執行
$ . ./calc.sh
或
$ source ./calc.sh$ type .. is a shell builtin$ type sourcesource 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.sh10000real 0m1.319suser 0m1.056ssys 0m0.036s$ time calc_let.sh10000real 0m1.426suser 0m1.176ssys 0m0.032s$ . 0m5.060ssys 0m14.177s$ time calc_bc.sh1000real 0m56.576suser 0m9.353ssys 0m24.618s$ time ./calc_awk.sh100real 0m11.6762620m.676260mm100real 0m11.676260m.
說明:
time命令可以用來統計命令執行時間,這部分時間包括總的運行時間,用戶空間執行時間,內核空間執行時間,它通過
ptrace系統呼叫實作。
透過上述比較可以發現
(())的運算效率最高。而
let作為Shell 內建指令,效率也很高,但是
expr,
bc,
awk的計算效率就比較低。所以,在Shell 本身能夠完成相關工作的情況下,建議優先使用Shell 本身提供的功能。但是Shell 本身無法完成的功能,例如浮點運算,所以就需要外部指令的幫助。另外,考慮到Shell 腳本的可移植性,在效能不是很關鍵的情況下,不要使用某些Shell 特有的語法。
let,
expr,
bc都可以用來求模,運算子都是
%,而
let和
bc可以用來求冪,運算子不一樣,前者是
**,後者是
^。例如:
$ expr 5 % 21$ let i=5%2$ echo $i1$ echo 5 % 2 | bc1$ ((i=5%2))$ echo $i1
$ let i=5**2$ echo $i25$ ((i=5**2))$ echo $i25$ echo 5^2 | bc25
進位轉換也是比較常用的操作,可以用
Bash的內建支援也可以用
bc來完成,例如把8 進制的11 轉換為10 進制,則可以:
$ echo obase=10;ibase=8;11 | bc -l9$ echo $((8#11))9
上面都是把某個進位的數轉換成10 進位的,如果要進行任意進位之間的轉換還是
bc比較靈活,因為它可以直接用
ibase和
obase分別指定進位來源和進位轉換目標。
如果要把某些字串以特定的進位表示,可以用
od命令,例如預設的分隔符
IFS包括空格、
TAB以及換行,可以用
man ascii佐證。
$ echo -n $IFS | od -c0000000 t n0000003$ echo -n $IFS | od -b0000000 040 011 0120000003
let和
expr都無法進行浮點運算,但是
bc和
awk可以。
$ 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 -l4.9349547554113836327198340369318406051597063986552438753727649177325495504159766011527078
當然也可以用
awk來計算:
$ echo 0.996293 | awk '{ printf(%sn, atan2(sqrt(1-$1^2),$1)*180/3.1415926535);}'4.93495
這裡隨機產生了一組測試數據,檔案名為
income.txt。
1 3 44902 5 38963 4 31124 4 47165 4 45786 6 53997 3 50898 6 30299 4 619510 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 -1income=$1awk '{ 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.txt7 1696.339 1548.751 1496.674 1179.005 1144.5010 1029.006 899.832 779.203 1029.006 899.832 779.203 7778.
補充:之前的
income.txt數據是隨機產生的。在做一些實驗時,往往需要隨機產生一些數據,在下一小節,我們將詳細介紹它。這裡是產生
income.txt數據的腳本:
#!/bin/bash# genrandomdata.shfor 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 $RANDOM81$ echo | awk '{srand(); printf(%f, rand());}'0.237788
說明:
srand()在無參數時,採用當前時間作為
rand()隨機數字產生器的一個
seed。
可以透過
RANDOM變數的縮放和
awk中
rand()的放大來實現。
$ expr $RANDOM / 128$ echo | awk '{srand(); printf(%dn, 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, defgate , and the time out of ping commandnet=192.168.1default_gateway=192.168.1.1over_time=2# check the current ipaddressping -c 1 $default_gateway -W $over_time[ $? -eq 0 ] && echo the current gateway -W $over_time[ $? -eq 0 ] && echo the current exdress is okey! &wg # 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 defaulturation route add default gw $default_gateway # check the new configuration route 10 - $default_gateway -W $over_time # if work, finish [ $? -eq 0 ] && breakdone
說明:如果你的預設網關位址不是
192.168.1.1,請自行配置
default_gateway(可以用
route -n命令查看),因為用
ifconfig設定位址時不能設定為網關位址,否則你的IP位址將會和網關一樣,導致整個網路無法正常運作。
其實透過一個循環就可以產生一系列數,但有相關工具為什麼不用呢!
seq就是這麼一個小工具,它可以產生一系列數,你可以指定數的遞增間隔,也可以指定相鄰兩個數之間的分割符。
$ seq 512345$ seq 1 512345$ seq 1 2 5135$ seq -s: 1 2 51:3:5$ seq 1 2 14135791113$ seq -w 1 2 14010301 1903013$ 1401:03:05:07:09:11:13$ seq -f 0x%g 1 50x10x20x30x40x5
一個比較典型的使用
seq的例子,建構一些特定格式的鏈接,然後用
wget下載這些內容:
$ for i in `seq -fhttp://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 ; done1 2 3 4 5
我們先給單字一個定義:由字母組成的單一或多個字元系列。
首先,統計每個單字出現的次數:
$ wget -c http://tinylab.org$ cat index.html | sed -es/[^a-zA-Z]/n/g | grep -v ^$ | sort | uniq -c
接著,統計出現頻率最高的前10個字:
$ wget -c http://tinylab.org$ cat index.html | sed -es/[^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 -es/[^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.shif [ $# -lt 1 ]; then echo Usage: basename $0 FILE WORDS .... exit -1fiFILE=$1((WORDS_NUM=$#-1))for n in $( seq $WORDS_NUM)do shift cat $FILE | sed -es/[^a-zA-Z]/n/g | grep -v ^$ | sort | grep ^$1$ | uniq -cdone
說明:
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.shif [ $# -lt 1 ]; then echo ERROR: you should input 2 words at least; echo Usage: basename $0 FILE WORDS .... exit -1fiFILE=$1((WORDS_NUM= $#-1))for n in $(seq $WORDS_NUM)do shift cat $FILE | sed -es/[^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 -es/[^a-zA-Z]/n/g | grep -v ^$ | sort | grep -E ^tinylab$|^linux$ | uniq -c 43 linux 175 tinylab
或者
$ cat index.html | sed -es/[^a-zA-Z]/n/g | grep -v ^$ | sort | egrep ^tinylab$|^linux$ | uniq -c 43 linux 175 tinylab
說明:需要注意到
sed命令可以直接處理文件,而無需通過
cat命令輸出以後再透過管道傳遞,這樣可以減少一個不必要的管道操作,所以上述命令可以簡化為:
$ sed -es/[^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 100100: 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來做這個工作比較,你會發現前者是那麼簡單和省事,而且給您以運用自如的感覺。