從本文開始,打算結(jié)合平時(shí)積累和進(jìn)一步實(shí)踐,通過一些范例來介紹Shell編程。因?yàn)榉独軌蚪o人以學(xué)有所用的感覺,而且給人以動(dòng)手實(shí)踐的機(jī)會(huì),從而激發(fā)人的學(xué)習(xí)熱情。
考慮到易讀性,這些范例將非常簡(jiǎn)單,但是實(shí)用,希望它們能夠成為我們解決日常問題的參照物或者是“茶余飯后”的小點(diǎn)心,當(dāng)然這些“點(diǎn)心”肯定還有值得探討、優(yōu)化的地方。
更復(fù)雜有趣的例子請(qǐng)參考 Advanced Bash-Scripting Guide (一本深入學(xué)習(xí) Shell 腳本藝術(shù)的書籍)。
該系列概要:
這一篇打算討論一下 Shell 編程中的基本數(shù)值運(yùn)算,這類運(yùn)算包括:
Shell 本身可以做整數(shù)運(yùn)算,復(fù)雜一些的運(yùn)算要通過外部命令實(shí)現(xiàn),比如 expr
,bc
,awk
等。另外,可通過 RANDOM
環(huán)境變量產(chǎn)生一個(gè)從 0 到 32767 的隨機(jī)數(shù),一些外部工具,比如 awk
可以通過 rand()
函數(shù)產(chǎn)生隨機(jī)數(shù)。而 seq
命令可以用來產(chǎn)生一個(gè)數(shù)列。下面對(duì)它們分別進(jìn)行介紹。
$ 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 之間有空格分開。如果進(jìn)行乘法運(yùn)算,需要對(duì)運(yùn)算符進(jìn)行轉(zhuǎn)義,否則 Shell 會(huì)把乘號(hào)解釋為通配符,導(dǎo)致語法錯(cuò)誤; awk
后面的 $1
和 $2
分別指 $i
和 1,即從左往右的第 1 個(gè)和第 2 個(gè)數(shù)。
用 Shell 的內(nèi)置命令查看各個(gè)命令的類型如下:
$ 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 內(nèi)置命令,其他幾個(gè)是外部命令,都在 /usr/bin
目錄下。而 expr
和 bc
因?yàn)閯傆眠^,已經(jīng)加載在內(nèi)存的 hash
表中。這將有利于我們理解在上一章介紹的腳本多種執(zhí)行方法背后的原理。
說明:如果要查看不同命令的幫助,對(duì)于 let
和 type
等 Shell 內(nèi)置命令,可以通過 Shell 的一個(gè)內(nèi)置命令 help
來查看相關(guān)幫助,而一些外部命令可以通過 Shell 的一個(gè)外部命令 man
來查看幫助,用法諸如 help let
,man expr
等。
#!/bin/bash
# calc.sh
i=0;
while [ $i -lt 10000 ]
do
((i++))
done
echo $i
說明:這里通過 while [ 條件表達(dá)式 ]; do .... done
循環(huán)來實(shí)現(xiàn)。-lt
是小于號(hào) <
,具體見 test
命令的用法:man test
。
如何執(zhí)行該腳本?
辦法一:直接把腳本文件當(dāng)成子 Shell (Bash)的一個(gè)參數(shù)傳入
$ bash calc.sh
$ type bash
bash is hashed (/bin/bash)
辦法二:是通過 bash
的內(nèi)置命令 .
或 source
執(zhí)行
$ . ./calc.sh
或
$ source ./calc.sh
$ type .
. is a shell builtin
$ type source
source is a shell builtin
辦法三:是修改文件為可執(zhí)行,直接在當(dāng)前 Shell 下執(zhí)行
$ chmod ./calc.sh
$ ./calc.sh
下面,逐一演示用其他方法計(jì)算變量加一,即把 ((i++))
行替換成下面的某一個(gè):
let i++;
i=$(expr $i + 1)
i=$(echo $i+1|bc)
i=$(echo "$i 1" | awk '{printf $1+$2;}')
比較計(jì)算時(shí)間如下:
$ 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
命令可以用來統(tǒng)計(jì)命令執(zhí)行時(shí)間,這部分時(shí)間包括總的運(yùn)行時(shí)間,用戶空間執(zhí)行時(shí)間,內(nèi)核空間執(zhí)行時(shí)間,它通過 ptrace
系統(tǒng)調(diào)用實(shí)現(xiàn)。
通過上述比較可以發(fā)現(xiàn) (())
的運(yùn)算效率最高。而 let
作為 Shell 內(nèi)置命令,效率也很高,但是 expr
,bc
,awk
的計(jì)算效率就比較低。所以,在 Shell 本身能夠完成相關(guān)工作的情況下,建議優(yōu)先使用 Shell 本身提供的功能。但是 Shell 本身無法完成的功能,比如浮點(diǎn)運(yùn)算,所以就需要外部命令的幫助。另外,考慮到 Shell 腳本的可移植性,在性能不是很關(guān)鍵的情況下,不要使用某些 Shell 特有的語法。
let
,expr
,bc
都可以用來求模,運(yùn)算符都是 %
,而 let
和 bc
可以用來求冪,運(yùn)算符不一樣,前者是 **
,后者是 ^
。例如:
$ 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
進(jìn)制轉(zhuǎn)換也是比較常用的操作,可以用 Bash
的內(nèi)置支持也可以用 bc
來完成,例如把 8 進(jìn)制的 11 轉(zhuǎn)換為 10 進(jìn)制,則可以:
$ echo "obase=10;ibase=8;11" | bc -l
9
$ echo $((8#11))
9
上面都是把某個(gè)進(jìn)制的數(shù)轉(zhuǎn)換為 10 進(jìn)制的,如果要進(jìn)行任意進(jìn)制之間的轉(zhuǎn)換還是 bc
比較靈活,因?yàn)樗梢灾苯佑?ibase
和 obase
分別指定進(jìn)制源和進(jìn)制轉(zhuǎn)換目標(biāo)。
如果要把某些字符串以特定的進(jìn)制表示,可以用 od
命令,例如默認(rèn)的分隔符 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
都無法進(jìn)行浮點(diǎn)運(yùn)算,但是 bc
和 awk
可以。
$ echo "scale=3; 1/13" | bc
.076
$ echo "1 13" | awk '{printf("%0.3fn",$1/$2)}'
0.077
說明: bc
在進(jìn)行浮點(diǎn)運(yùn)算時(shí)需指定精度,否則默認(rèn)為 0,即進(jìn)行浮點(diǎn)運(yùn)算時(shí),默認(rèn)結(jié)果只保留整數(shù)。而 awk
在控制小數(shù)位數(shù)時(shí)非常靈活,僅僅通過 printf
的格式控制就可以實(shí)現(xiàn)。
補(bǔ)充:在用 bc
進(jìn)行運(yùn)算時(shí),如果不用 scale
指定精度,而在 bc
后加上 -l
選項(xiàng),也可以進(jìn)行浮點(diǎn)運(yùn)算,只不過這時(shí)的默認(rèn)精度是 20 位。例如:
$ echo 1/13100 | bc -l
.00007633587786259541
用 bc -l
計(jì)算,可以獲得高精度:
$ export cos=0.996293; echo "scale=100; a(sqrt(1-$cos^2)/$cos)*180/(a(1)*4)" | bc -l
4.934954755411383632719834036931840605159706398655243875372764917732
5495504159766011527078286004072131
當(dāng)然也可以用 awk
來計(jì)算:
$ echo 0.996293 | awk '{ printf("%s\n", atan2(sqrt(1-$1^2),$1)*180/3.1415926535);}'
4.93495
在這里隨機(jī)產(chǎn)生了一組測(cè)試數(shù)據(jù),文件名為 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
說明:上面的三列數(shù)據(jù)分別是家庭編號(hào)、家庭人數(shù)、家庭月總收入。
分析:為了求月均收入最高家庭,需要對(duì)后面兩列數(shù)進(jìn)行除法運(yùn)算,即求出每個(gè)家庭的月均收入,然后按照月均收入排序,找出收入最高家庭。
實(shí)現(xiàn):
#!/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 ]
:要求至少輸入一個(gè)參數(shù),$#
是 Shell 中傳入?yún)?shù)的個(gè)數(shù)[ ! -f $1 ]
:要求輸入?yún)?shù)是一個(gè)文件,-f
的用法見 test
命令,man test
income=$1
:把輸入?yún)?shù)賦給 income 變量,再作為 awk
的參數(shù),即需處理的文件awk
:用文件第三列除以第二列,求出月均收入,考慮到精確性,保留了兩位精度sort -k 2 -n -r
:這里對(duì)結(jié)果的 awk
結(jié)果的第二列 -k 2
,即月均收入進(jìn)行排序,按照數(shù)字排序 -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
補(bǔ)充:之前的 income.txt
數(shù)據(jù)是隨機(jī)產(chǎn)生的。在做一些實(shí)驗(yàn)時(shí),往往需要隨機(jī)產(chǎn)生一些數(shù)據(jù),在下一小節(jié),我們將詳細(xì)介紹它。這里是產(chǎn)生 income.txt
數(shù)據(jù)的腳本:
#!/bin/bash
# genrandomdata.sh
for i in $(seq 1 10)
do
echo $i $(($RANDOM/8192+3)) $((RANDOM/10+3000))
done
說明:上述腳本中還用到seq
命令產(chǎn)生從1到10的一列數(shù),這個(gè)命令的詳細(xì)用法在該篇最后一節(jié)也會(huì)進(jìn)一步介紹。
環(huán)境變量 RANDOM
產(chǎn)生從 0 到 32767 的隨機(jī)數(shù),而 awk
的 rand()
函數(shù)可以產(chǎn)生 0 到 1 之間的隨機(jī)數(shù)。
$ echo $RANDOM
81
$ echo "" | awk '{srand(); printf("%f", rand());}'
0.237788
說明: srand()
在無參數(shù)時(shí),采用當(dāng)前時(shí)間作為 rand()
隨機(jī)數(shù)產(chǎn)生器的一個(gè) seed
。
可以通過 RANDOM
變量的縮放和 awk
中 rand()
的放大來實(shí)現(xiàn)。
$ expr $RANDOM / 128
$ echo "" | awk '{srand(); printf("%d\n", rand()*255);}'
思考:如果要隨機(jī)產(chǎn)生某個(gè) IP 段的 IP 地址,該如何做呢?看例子:友善地獲取一個(gè)可用的 IP 地址。
#!/bin/bash
# getip.sh -- get an usable ipaddress automatically
# author: falcon <zhangjinw@gmail.com>
# 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_gateway -W $over_time
# if work, finish
[ $? -eq 0 ] && break
done
說明:如果你的默認(rèn)網(wǎng)關(guān)地址不是 192.168.1.1
,請(qǐng)自行配置 default_gateway
(可以用 route -n
命令查看),因?yàn)橛?ifconfig
配置地址時(shí)不能配置為網(wǎng)關(guān)地址,否則你的IP地址將和網(wǎng)關(guān)一樣,導(dǎo)致整個(gè)網(wǎng)絡(luò)不能正常工作。
其實(shí)通過一個(gè)循環(huán)就可以產(chǎn)生一系列數(shù),但是有相關(guān)工具為什么不用呢!seq
就是這么一個(gè)小工具,它可以產(chǎn)生一系列數(shù),你可以指定數(shù)的遞增間隔,也可以指定相鄰兩個(gè)數(shù)之間的分割符。
$ 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
一個(gè)比較典型的使用 seq
的例子,構(gòu)造一些特定格式的鏈接,然后用 wget
下載這些內(nèi)容:
$ 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
補(bǔ)充:在 Bash
版本 3 以上,在 for
循環(huán)的 in
后面,可以直接通過 {1..5}
更簡(jiǎn)潔地產(chǎn)生自 1 到 5 的數(shù)字(注意,1 和 5 之間只有兩個(gè)點(diǎn)),例如:
$ for i in {1..5}; do echo -n "$i "; done
1 2 3 4 5
我們先給單詞一個(gè)定義:由字母組成的單個(gè)或者多個(gè)字符系列。
首先,統(tǒng)計(jì)每個(gè)單詞出現(xiàn)的次數(shù):
$ wget -c http://tinylab.org
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c
接著,統(tǒng)計(jì)出現(xiàn)頻率最高的前10個(gè)單詞:
$ 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 文件里的內(nèi)容sed -e "s/[^a-zA-Z]/\n/g"
: 把非字母字符替換成空格,只保留字母字符grep -v ^$
: 去掉空行sort
: 排序uniq -c
:統(tǒng)計(jì)相同行的個(gè)數(shù),即每個(gè)單詞的個(gè)數(shù)sort -n -k 1 -r
:按照第一列 -k 1
的數(shù)字 -n
逆序 -r
排序head -10
:取出前十行可以考慮采取兩種辦法:
不過,這兩種辦法都可以通過下面的結(jié)構(gòu)來實(shí)現(xiàn)。先看辦法一:
#!/bin/bash
# statistic_words.sh
if [ $# -lt 1 ]; then
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 條件部分
:要求至少兩個(gè)參數(shù),第一個(gè)單詞文件,之后參數(shù)為要統(tǒng)計(jì)的單詞FILE=$1
: 獲取文件名,即腳本之后的第一個(gè)字符串((WORDS_NUM=$#-1))
:獲取單詞個(gè)數(shù),即總的參數(shù)個(gè)數(shù) $#
減去文件名參數(shù)(1個(gè))for 循環(huán)部分
:首先通過 seq
產(chǎn)生需要統(tǒng)計(jì)的單詞個(gè)數(shù)系列,shift
是 Shell 內(nèi)置變量(請(qǐng)通過 help shift
獲取幫助),它把用戶從命令行中傳入的參數(shù)依次往后移動(dòng)位置,并把當(dāng)前參數(shù)作為第一個(gè)參數(shù)即 $1
,這樣通過 $1
就可以遍歷用戶所有輸入的單詞(仔細(xì)一想,這里貌似有數(shù)組下標(biāo)的味道)。你可以考慮把 shift
之后的那句替換成 echo $1
測(cè)試 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
說明:很明顯,辦法一的效率要高很多,因?yàn)樗崆罢页隽诵枰y(tǒng)計(jì)的單詞,然后再統(tǒng)計(jì),而后者則不然。實(shí)際上,如果使用 grep
的 -E
選項(xiàng),我們無須引入循環(huán),而用一條命令就可以搞定:
$ 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
命令輸出以后再通過管道傳遞,這樣可以減少一個(gè)不必要的管道操作,所以上述命令可以簡(jiǎn)化為:
$ 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
是多么有用,它們本身雖然只完成簡(jiǎn)單的功能,但是通過一定的組合,就可以實(shí)現(xiàn)各種五花八門的事情啦。對(duì)了,統(tǒng)計(jì)單詞還有個(gè)非常有用的命令 wc -w
,需要用到的時(shí)候也可以用它。
補(bǔ)充:在 Advanced Bash-Scripting Guide 一書中還提到 jot
命令和 factor
命令,由于機(jī)器上沒有,所以沒有測(cè)試,factor
命令可以產(chǎn)生某個(gè)數(shù)的所有素?cái)?shù)。如:
$ factor 100
100: 2 2 5 5
到這里,Shell 編程范例之?dāng)?shù)值計(jì)算就結(jié)束啦。該篇主要介紹了:
sed
,awk
,grep
,uniq
,sort
等IP
地址;統(tǒng)計(jì)單詞個(gè)數(shù)如果您有時(shí)間,請(qǐng)溫習(xí)之。
幾個(gè) Shell 討論區(qū)
大概花了 3 個(gè)多小時(shí)才寫完,目前是 23:33,該回宿舍睡覺啦,明天起來修改錯(cuò)別字和補(bǔ)充一些內(nèi)容,朋友們晚安!
10 月 31 號(hào),修改部分措辭,增加一篇統(tǒng)計(jì)家庭月均收入的范例,添加總結(jié)和參考資料,并用附錄所有代碼。
Shell 編程是一件非常有趣的事情,如果您想一想:上面計(jì)算家庭月均收入的例子,然后和用 M$ Excel
來做這個(gè)工作比較,你會(huì)發(fā)現(xiàn)前者是那么簡(jiǎn)單和省事,而且給您以運(yùn)用自如的感覺。
更多建議: