[授權發表]Shell編程範例之數值運算

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和22分別指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地址;統計單詞個數
  • 其他:相關的用法,比如命令列表,條件測試等,在上述範例中都已經涉及,請認真閱讀之

如果您有時間,請溫習之。

資料

後記

大概花了3個多小時才寫完,目前是23:33,該回宿舍睡覺啦,明天起來修改錯別字和補充一些內容,朋友們晚安!

10月31號,修改部分措辭,增加一篇統計家庭月均收入的範例,添加總結和參考資料,並用附錄所有代碼。

SHELL編程是一件非常有趣的事情,如果您想一想:上面計算家庭月均收入的例子,然後和用M$ Excel來做這個工作比較,你會發現前者是那麼簡單和省事,而且給您以運用自如的感覺。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章