? ? Shell概述
Shell是一種具備特殊功能的程序,它提供了用戶與內(nèi)核進行交互操作的一種接口。它接收用戶輸入的命令,并把它送入內(nèi)核去執(zhí)行。內(nèi)核是Linux系統(tǒng)的心臟,從開機自檢就駐留在計算機的內(nèi)存中,直到計算機關(guān)閉為止,而用戶的應用程序存儲在計算機的硬盤上,僅當需要時才被調(diào)入內(nèi)存。Shell是一種應用程序,當用戶登錄Linux系統(tǒng)時,Shell就會被調(diào)入內(nèi)存去執(zhí)行。Shell獨立于內(nèi)核,它是連接內(nèi)核和應用程序的橋梁,并由輸入設備讀取命令,再將其轉(zhuǎn)為計算機可以理解的機械碼,Linux內(nèi)核才能執(zhí)行該命令。
優(yōu)勢
Shell腳本語言的好處是簡單、易學、易用,適合處理文件和目錄之類的對象,以簡單的方式快速完成某些復雜的事情通常是創(chuàng)建腳本的重要原則,腳本語言的特性可以總結(jié)為以下幾個方面:
語法和結(jié)構(gòu)通常比較簡單。
學習和使用通常比較簡單,
通常以容易修改程序的“解釋”作為運行方式,而不需要“編譯。
程序的開發(fā)產(chǎn)能優(yōu)于運行效能。
Shell腳本語言是Linux/Unix系統(tǒng)上一種重要的腳本語言,在Linux/Unix領域應用極為廣泛,熟練掌握Shell腳本語言是一個優(yōu)秀的Linux/Unix開發(fā)者和系統(tǒng)管理員必經(jīng)之路。利用Shell腳本語言可以簡潔地實現(xiàn)復雜的操作,而且Shell腳本程序往往可以在不同版本的Linux/Unix系統(tǒng)上通用。
Shell編程
基本格式
Shell腳本的文件名后綴通常是.sh (當然你也可以使用其他后綴或者沒有后綴,.sh是為了規(guī)范)
程序編寫格式:
[java] view plain copy#!/bin/bash
# 注釋使用#號
代碼示例:
[java] view plain copy//使用vi編輯器編寫shell腳本(a.sh不存在則會新建)
vi a.sh
進入vi編輯模式后編寫執(zhí)行代碼
[java] view plain copy//固定格式,記住就可以了
#!/bin/bash
//執(zhí)行的代碼
echo Hello World
賦予權(quán)限并執(zhí)行:
[java] view plain copy//賦予可執(zhí)行權(quán)限
chmod +x a.sh
//執(zhí)行(調(diào)用/bin/bash執(zhí)行a.sh腳本)
。/a.sh
執(zhí)行結(jié)果:
下面是幾種運行情況:
[java] view plain copya.sh
這樣的話需要保證腳本具有執(zhí)行權(quán)限并且在環(huán)境變量PATH中有(。),這樣在執(zhí)行的時候會先從當前目錄查找。
[java] view plain copy./a.sh
只要保證這個腳本具有執(zhí)行權(quán)限即可
[java] view plain copy/usr/local/a.sh
只要保證這個腳本具有執(zhí)行權(quán)限即可
[java] view plain copybash a.sh
直接可以執(zhí)行,甚至這個腳本文件中的第一行都可以不引入/bin/bash,它是將hello.sh作為參數(shù)傳給bash命令來執(zhí)行的。
[java] view plain copybash -x /path/to/aa.sh
bash的單步執(zhí)行
[java] view plain copybash -n /path/to/aa.sh
bash語法檢查
變量
變量不需要聲明,初始化不需要指定類型
變量命名
1、只能使用數(shù)字,字母和下劃線,且不能以數(shù)字開頭
2、變量名區(qū)分大小寫
3、建議命令要通俗易懂
注意:變量賦值是通過等號(=)進行賦值,在變量、等號和值之間不能出現(xiàn)空格。
顯示變量值使用echo命令(類似于java中的system.out) ,加上$變量名,也可以使用${變量名}
例如:
[java] view plain copyecho $JAVA_HOME
echo ${JAVA_HOME}
變量的申明和使用:
變量分類:
Shell變量有這幾類:本地變量、環(huán)境變量、局部變量、位置變量、特殊變量。
本地變量:
只對當前shell進程有效的,對當前進程的子進程和其它shell進程無效。
定義:VAR_NAME=VALUE
變量引用:${VAR_NAME} 或者 $VAR_NAME
取消變量:unset VAR_NAME
相當于java中的私有變量(private),只能當前類使用,子類和其他類都無法使用。
比如在一個bash命令窗口下再使用bash,則變成了子進程,本地變量不會被這個子進程所訪問。
環(huán)境變量:
自定義的環(huán)境變量對當前shell進程及其子shell進程有效,對其它的shell進程無效
定義:export VAR_NAME=VALUE
對所有shell進程都有效需要配置到配置文件中
[java] view plain copyvi /etc/profile
source /etc/profile
相當于java中的protected修飾符,對當前類,子孫類,以及同一個包下面可以共用。
和windows中的環(huán)境變量比較類似
自定義的環(huán)境變量:
局部變量:
在函數(shù)中調(diào)用,函數(shù)執(zhí)行結(jié)束,變量就會消失
對shell腳本中某代碼片段有效
定義:local VAR_NAME=VALUE
相當于java代碼中某一個方法中定義的局部變量,只對這個方法有效。
位置變量:
比如腳本中的參數(shù):
$0:腳本自身
$1:腳本的第一個參數(shù)
$2:腳本的第二個參數(shù)
相當于java中main函數(shù)中的args參數(shù),可以獲取外部參數(shù)。
特殊變量:
$?:接收上一條命令的返回狀態(tài)碼
返回狀態(tài)碼在0-255之間
$#:參數(shù)個數(shù)
$*:或者$@:所有的參數(shù)
$$:獲取當前shell的進程號(PID)(可以實現(xiàn)腳本自殺)(或者使用exit命令直接退出也可以使用exit [num])
引號
Shell編程中有三類引號:單引號、雙引號、反引號。
‘’單引號不解析變量
[java] view plain copyecho ‘$name’
“”雙引號會解析變量
[java] view plain copyecho “$name”
``反引號是執(zhí)行并引用一個命令的執(zhí)行結(jié)果,類似于$(。。。)
[java] view plain copyecho `$name`
示例:
循環(huán)
for循環(huán)
通過使用一個變量去遍歷給定列表中的每個元素,在每次變量賦值時執(zhí)行一次循環(huán)體,直至賦值完成所有元素退出循環(huán)
格式1
[java] view plain copyfor ((i=0;i《10;i++))
do
。。。
Done
格式2
[java] view plain copyfor i in 0 1 2 3 4 5 6 7 8 9
do
。。。
Done
格式3
[java] view plain copyfor i in {0..9}
do
。。。
done
注意:for i in {0..9} 等于for i in {0..9..1} , 第三個參數(shù)為跨步。
例如:
{0..9..2} 表示 0,2,4,6,8
while循環(huán)
適用于循環(huán)次數(shù)未知,或不便用for直接生成較大的列表時
格式:
[java] view plain copywhile 測試條件
do
循環(huán)體
done
如果測試條件為“真”,則進入循環(huán),測試條件為假,則退出循環(huán)。
打印結(jié)果為0~9.
循環(huán)控制
循環(huán)控制命令——break
break命令是在處理過程中跳出循環(huán)的一種簡單方法,可以使用break命令退出任何類型的循環(huán),包括while循環(huán)和for循環(huán)
循環(huán)控制命令——continue
continue命令是一種提前停止循環(huán)內(nèi)命令,而不完全終止循環(huán)的方法,這就需要在循環(huán)內(nèi)設置shell不執(zhí)行命令的條件
條件
bash條件測試
格式:
[java] view plain copytest EXPR
[ EXPR ]:注意中括號和表達式之間的空格
整型測試:
-gt:大于:
-lt:小于
-ge:大于等于
-le:小于等于
-eq:等于
-ne:不等于
例如[ $num1 -gt $num2 ]或者test $num1 -gt $num2
字符串測試:
=:等于,例如判斷變量是否為空 [ “$str” = “” ] 或者[ -z $str ]
!=:不等于
判斷
if判斷:
單分支
[java] view plain copy if 測試條件;then
選擇分支
fi
雙分支
[java] view plain copyif 測試條件
then
選擇分支1
else
選擇分支2
fi
多分支
[java] view plain copyif 條件1; then
分支1
elif 條件2; then
分支2
elif 條件3; then
分支3
。。。
else
分支n
i
雙分支示例:
Case判斷
有多個測試條件時,case語句會使得語法結(jié)構(gòu)更清晰
格式:
[java] view plain copycase 變量引用 in
PATTERN1)
分支1
;;
PATTERN2)
分支2
;;
。。。
*)
分支n
;;
esac
PATTERN :類同于文件名通配機制,但支持使用|表示或者
a|b:a或者b
*:匹配任意長度的任意字符
?:匹配任意單個字符
[a-z]:指定范圍內(nèi)的任意單個字符
示例:
算術(shù)運算
[java] view plain copylet varName=算術(shù)表達式
varName=$[算術(shù)表達式]
varName=$((算術(shù)表達式))
varName=`expr $num1 + $num2`
使用這種格式要注意兩個數(shù)字和+號中間要有空格。
示例:
邏輯運算符
if [ 條件A && 條件B ] 在shell中怎么寫?
if [ 條件A && 條件B ];then 是不對的
解決方法:
(1)需要用到shell中的邏輯操作符
-a 與
-o 或
! 非
如if [ 條件A -a 條件B ]
(2)if [ 條件A ] && [條件B ]
(3)if((A&&B))
(4)if [[ A&&B ]]
自定義函數(shù)
格式:
[java] view plain copyfunction 函數(shù)名(){
。。。
}
引用自定義函數(shù)文件時,使用source func.sh
有利于代碼的重用性
函數(shù)傳遞參數(shù)(可以使用類似于Java中的args,args[1]代表Shell中的$1)
函數(shù)的返回值,只能是數(shù)字
read
read命令接收標準輸入(鍵盤)的輸入,或者其他文件描述符的輸入。得到輸入后,read命令將數(shù)據(jù)放入一個標準變量中。
格式
[java] view plain copyread VAR_NAME
read如果后面不指定變量,那么read命令會將接收到的數(shù)據(jù)放置在環(huán)境變量REPLY中
[java] view plain copy#表示輸入時的提示字符串:
read -p “Enter your name:” VAR_NAME
[java] view plain copy# -t表示輸入等待的時間
read -t 5 -p “enter your name:” VAR_NAME
[java] view plain copy# -s 表示安全輸入,鍵入密碼時不會顯示
read -s -p “Enter your password: ” pass
declare
用來限定變量的屬性
-r 只讀
-i 整數(shù):某些算術(shù)計算允許在被聲明為整數(shù)的變量中完成,而不需要特別使用expr或let來完成。
-a 數(shù)組
示例:
字符串操作
獲取長度:
[java] view plain copy${#VAR_NAME}
字符串截取
[java] view plain copy${variable:offset:length}或者${variable:offset}
取尾部的指定個數(shù)的字符
[java] view plain copy${variable: -length}:注意冒號后面有空格
大小寫轉(zhuǎn)換
小--》大:
[java] view plain copy${variable^^}
大--》小:
[java] view plain copy${variable,,}
示例:
數(shù)組
定義:declare -a:表示定義普通數(shù)組
特點
支持稀疏格式
僅支持一維數(shù)組
數(shù)組賦值方式
一次對一個元素賦值a[0]=$RANDOM
一次對多個元素賦值a=(a b c d)
按索引進行賦值a=([0]=a [3]=b [1]=c)
使用read命令read -a ARRAY_NAME查看元素
[java] view plain copy${ARRAY[index]}:查看數(shù)組指定角標的元素
${ARRAY}:查看數(shù)組的第一個元素
${ARRAY[*]}或者${ARRAY[@]}:查看數(shù)組的所有元素
獲取數(shù)組的長度
[java] view plain copy${#ARRAY[*]}
${#ARRAY[@]}
獲取數(shù)組內(nèi)元素的長度
[java] view plain copy${#ARRAY[0]}
注意:${#ARRAY[0]}表示獲取數(shù)組中的第一個元素的長度,等于${#ARRAY}
從數(shù)組中獲取某一片段之內(nèi)的元素(操作類似于字符串操作)
格式:
[java] view plain copy${ARRAY[@]:offset:length}
offset:偏移的元素個數(shù)
length:取出的元素的個數(shù)
${ARRAY[@]:offset:length}:取出偏移量后的指定個數(shù)的元素
${ARRAY[@]:offset}:取出數(shù)組中偏移量后的所有元素
數(shù)組刪除元素:
[java] view plain copyunset ARRAY[index]
示例:
其他命令
date
顯示當前時間
格式化輸出 +%Y-%m-%d
格式%s表示自1970-01-01 00:00:00以來的秒數(shù)
指定時間輸出 --date=‘2009-01-01 11:11:11’
指定時間輸出 --date=‘3 days ago’ (3天之前,3天之后可以用-3)
示例:
后臺運行腳本
在腳本后面加一個&
[java] view plain copytest.sh &
這樣的話雖然可以在后臺運行,但是當用戶注銷(logout)或者網(wǎng)絡斷開時,終端會收到Linux HUP信號(hangup)信號從而關(guān)閉其所有子進程
nohup命令
不掛斷的運行命令,忽略所有掛斷(hangup)信號
[java] view plain copynohup test.sh &
nohup會忽略進程的hangup掛斷信號,所以關(guān)閉當前會話窗口不會停止這個進程的執(zhí)行。
nohup會在當前執(zhí)行的目錄生成一個nohup.out日志文件
標準輸入、輸出、錯誤、重定向
標準輸入、輸出、錯誤可以使用文件描述符0、1、2引用
使用重定向可以把信息重定向到其他位置
ls 》file 或者 ls 1》file(ls 》》file)
lk 2》file(lk是一個錯誤命令)
ls 》file 2》&1
ls 》 /dev/null(把輸出信息重定向到無底洞)
例子:
[java] view plain copycommand 》/dev/null 2》&1
Crontab定時器
linux下的定時任務
編輯使用crontab -e
一共6列,分別是:分 時 日 月 周 命令
查看crontab執(zhí)行日志
[java] view plain copytail -f /var/log/cron
必須打開rsyslog服務cron文件中才會有執(zhí)行日志(service rsyslog status)
[java] view plain copytail -f /var/spool/mail/root(查看crontab最近的執(zhí)行情況)
查看cron服務狀態(tài)
[java] view plain copyservice crond status
啟動cron服務
[java] view plain copyservice crond start
小結(jié)及示例:
基本格式 :
* * * * * command
分 時 日 月 周 命令
第1列表示分鐘1~59 每分鐘用*或者 */1表示
第2列表示小時1~23(0表示0點)
第3列表示日期1~31
第4列表示月份1~12
第5列標識號星期0~6(0表示星期天)
第6列要運行的命令
crontab文件的一些例子:
30 21 * * * /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每晚的21:30重啟apache。
45 4 1,10,22 * * /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每月1、10、22日的4 : 45重啟apache。
10 1 * * 6,0 /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每周六、周日的1 : 10重啟apache。
0,30 18-23 * * * /usr/local/etc/rc.d/lighttpd restart
上面的例子表示在每天18 : 00至23 : 00之間每隔30分鐘重啟apache。
0 23 * * 6 /usr/local/etc/rc.d/lighttpd restart
上面的例子表示每星期六的11 : 00 pm重啟apache。
* */1 * * * /usr/local/etc/rc.d/lighttpd restart
每一小時重啟apache
* 23-7/1 * * * /usr/local/etc/rc.d/lighttpd restart
晚上11點到早上7點之間,每隔一小時重啟apache
0 11 4 * mon-wed /usr/local/etc/rc.d/lighttpd restart
每月的4號與每周一到周三的11點重啟apache
0 4 1 jan * /usr/local/etc/rc.d/lighttpd restart
一月一號的4點重啟apache
ps和jps
ps:用來顯示進程的相關(guān)信息
ps顯示當前shell啟動的所有進程
ps -e顯示系統(tǒng)中所有進程
ps -ef|grep java
jps:類似linux的ps命令,不同的是ps是用來顯示所有進程,而jps只顯示java進程,準確的說是顯示當前用戶已啟動的部分java進程信息,信息包括進程號和簡短的進程command。
問題:某個java進程已經(jīng)啟動,用jps卻顯示不了該進程進程號,使用ps -ef|grep java卻可以看到?
java程序啟動后,默認(請注意是默認)會在/tmp/hsperfdata_userName目錄下以該進程的id為文件名新建文件,并在該文件中存儲jvm運行的相關(guān)信息,其中的userName為當前的用戶名,/tmp/hsperfdata_userName目錄會存放該用戶所有已經(jīng)啟動的java進程信息。而jps、jconsole、jvisualvm等工具的數(shù)據(jù)來源就是這個文件(/tmp/hsperfdata_userName/pid)。所以當該文件不存在或是無法讀取時就會出現(xiàn)jps無法查看該進程號。
原因:1,磁盤讀寫、目錄權(quán)限問題。2,臨時文件丟失,被刪除或是定期清理。3,java進程信息文件存儲地址被設置,不在/tmp目錄下
登錄Shell和交互shell
交互式的:顧名思義,這種shell中的命令時由用戶從鍵盤交互式地輸入的,運行的結(jié)果也能夠輸出到終端顯示給用戶看。
非交互式的:這種shell可能由某些自動化過程啟動,不能直接從請求用戶的輸入,也不能直接輸出結(jié)果給終端用戶看。輸出最好寫到文件。比如使用Shell腳本。
登錄式:意思是這種是在某用戶由/bin/login登陸進系統(tǒng)后啟動的shell,跟這個用戶綁定。這個shell是用戶登陸后啟動的第一個進程。login進程在啟動shell時傳遞第0個參數(shù)指明shell的名字,該參數(shù)第一個字符為“-”,指明這是一個login shell。比如對bash而言,啟動參數(shù)為“-bash”。
非登錄式:不需login而由某些程序啟動的shell。傳遞給shell的參數(shù),是沒有‘-’前綴的。還以Bash為例,當以非login方式啟動時,它會調(diào)用~/.bashrc,隨后~/.bashrc中調(diào)用/etc/bashrc,最后/etc/bashrc調(diào)用所有/etc/profile.d目錄下的腳本。
一旦打開一個交互式login shell,或者以--login選項登錄的非交互式shell,都會首先加載并執(zhí)行/etc/profile中的命令,然后再依次加載~/.bash_profile, ~/.bash_login, 和~/.profile中的命令。
當bash以login shell啟動時,它會執(zhí)行/etc/profile中的命令,然后/etc/profile調(diào)用/etc/profile.d目錄下的所有腳本;然后執(zhí)行~/.bash_profile,~/.bash_profile調(diào)用~/.bashrc,最后~/.bashrc又調(diào)用/etc/bashrc。要識別一個shell是否為login shell,只需在該shell下執(zhí)行echo $0。
注意: /etc/profile中的設置只對Login Shell生效,而crontab運行腳本的shell環(huán)境是non-login的,不會加載/etc/profile的設置。
Shell應用示例
根據(jù)時間創(chuàng)建文件夾
需求:創(chuàng)建10個目錄,目錄名稱以當天時間開頭,后面拼上目錄編碼
例如:1970-01-01_1
編寫腳本monitor.sh
持續(xù)觀察服務器每天的運行狀態(tài),需要結(jié)合shell腳本程序和計劃任務,定期跟蹤記錄不同時段服務器的cpu負載,內(nèi)存,交換空間,磁盤使用量等信息
[java] view plain copy#!/bin/bash
#this is the second script!
day_time=`date+“%F %R”`
cpu_test=`uptime`
mem_test=`free -m | grep “mem” | awk ‘{print $2}’`
swap_test=`free -m | grep “mem” | awk ‘{print $4}’`
disk_test=`df -hT`
user_test=`last -n 10`
echo “now is $day_time”
echo “%cpu is $cpu_test”
echo “Numbet of Mem size(MB) is $mem_test”
echo “Number of swap size(MB) is $swap_test”
echo “the disk shiyong qingkuang is $disk_test”
echo “the users login qingkuang is $user_test”
設置cron任務
[java] view plain copy*/15 * * * * bash /monitor.sh
55 23 * * * tar cxf /var/log/runrec /var/log/running.today && --remove-files
SHELL編程之常用技巧
/dev和/proc目錄
dev目錄是系統(tǒng)中集中用來存放設備文件的目錄。除了設備文件以外,系統(tǒng)中也有不少特殊的功能通過設備的形式表現(xiàn)出來。設備文件是一種特殊的文件,它們實際上是驅(qū)動程序的接口。在Linux操作系統(tǒng)中,很多設備都是通過設備文件的方式為進程提供了輸入、輸出的調(diào)用標準,這也符合UNIX的“一切皆文件”的設計原則。所以,對于設備文件來說,文件名和路徑其實都不重要,最重要的使其主設備號和輔助設備號,就是用ls -l命令顯示出來的原本應該出現(xiàn)在文件大小位置上的兩個數(shù)字,比如下面命令顯示的8和0:
[zorro@zorrozou-pc0 bash]$ ls -l /dev/sda
brw-rw---- 1 root disk 8, 0 5月 12 10:47 /dev/sda12
設備文件的主設備號對應了這種設備所使用的驅(qū)動是哪個,而輔助設備號則表示使用同一種驅(qū)動的設備編號。我們可以使用mknod命令手動創(chuàng)建一個設備文件:
[zorro@zorrozou-pc0 bash]$ sudo mknod harddisk b 8 0
[zorro@zorrozou-pc0 bash]$ ls -l harddisk
brw-r--r-- 1 root root 8, 0 5月 18 09:49 harddisk123
這樣我們就創(chuàng)建了一個設備文件叫harddisk,實際上它跟/dev/sda是同一個設備,因為它們對應的設備驅(qū)動和編號都一樣。所以這個設備實際上是跟sda相同功能的設備。
系統(tǒng)還給我們提供了幾個有特殊功能的設備文件,在bash編程的時候可能會經(jīng)常用到:
/dev/null:黑洞文件。可以對它重定向如何輸出。
/dev/zero:0發(fā)生器。可以產(chǎn)生二進制的0,產(chǎn)生多少根使用時間長度有關(guān)。我們經(jīng)常用這個文件來產(chǎn)生大文件進行某些測試,如:
[zorro@zorrozou-pc0 bash]$ dd if=/dev/zero of=。/bigfile bs=1M count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.3501 s, 3.1 GB/s1234
dd命令也是我們在bash編程中可能會經(jīng)常使用到的命令。
/dev/random:Linux下的random文件是一個根據(jù)計算機背景噪聲而產(chǎn)生隨機數(shù)的真隨機數(shù)發(fā)生器。所以,如果容納噪聲數(shù)據(jù)的熵池空了,那么對文件的讀取會出現(xiàn)阻塞。
/dev/urandom:是一個偽隨機數(shù)發(fā)生器。實際上在Linux的視線中,urandom產(chǎn)生隨機數(shù)的方法根random一樣,只是它可以重復使用熵池中的數(shù)據(jù)。這兩個文件在不同的類unix系統(tǒng)中可能實現(xiàn)方法不同,請注意它們的區(qū)別。
/dev/tcp & /dev/udp:這兩個神奇的目錄為bash編程提供了一種可以進行網(wǎng)絡編程的功能。在bash程序中使用/dev/tcp/ip/port的方式就可以創(chuàng)建一個scoket作為客戶端去連接服務端的ip:port。我們用一個檢查http協(xié)議的80端口是否打開的例子來說明它的使用方法:
[zorro@zorrozou-pc0 bash]$ cat tcp.sh
#!/bin/bash
ipaddr=127.0.0.1
port=80
if ! exec 5《》 /dev/tcp/$ipaddr/$port
then
exit 1
fi
echo -e “GET / HTTP/1.0\n” 》&5
cat 《&51234567891011121314
ipaddr的部分還可以寫一個主機名。大家可以用此腳本分別在本機打開web服務和不打開的情況下分別執(zhí)行觀察是什么效果。
/proc是另一個我們經(jīng)常使用的目錄。這個目錄完全是內(nèi)核虛擬的。內(nèi)核將一些系統(tǒng)信息都放在/proc目錄下一文件和文本的方式顯示出來,如:/proc/cpuinfo、/proc/meminfo。我們可以使用man 5 proc來查詢這個目錄下文件的作用。
函數(shù)和遞歸
我們已經(jīng)接觸過函數(shù)的概念了,在bash編程中,函數(shù)無非是將一串命令起了個名字,后續(xù)想要調(diào)用這一串命令就可以直接寫函數(shù)的名字了。在語法上定義一個函數(shù)的方法是:
name () compound-command [redirection]
function name [()] compound-command [redirection]12
我們可以加function關(guān)鍵字顯式的定義一個函數(shù),也可以不加。函數(shù)在定義的時候可以直接在后面加上重定向的處理。這里還需要特殊說明的是函數(shù)的參數(shù)處理和局部變量,請看下面腳本:
[zorro@zorrozou-pc0 bash]$ cat function.sh |awk ‘{print “\t”$0}’
#!/bin/bash
aaa=1000
arg_proc () {
echo “Function begin:”
local aaa=2000
echo $1
echo $2
echo $3
echo $*
echo $@
echo $aaa
echo “Function end!”
}
echo “Script bugin:”
echo $1
echo $2
echo $3
echo $*
echo $@
echo $aaa
arg_proc aaa bbb ccc ddd eee fff
echo $1
echo $2
echo $3
echo $*
echo $@
echo $aaa
echo “Script end!”12345678910111213141516171819202122232425262728293031323334
我們帶-x參數(shù)執(zhí)行一下:
+ aaa=1000
+ echo ‘Script bugin:’
Script bugin:
+ echo 111
111
+ echo 222
222
+ echo 333
333
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 1000
1000
+ arg_proc aaa bbb ccc ddd eee fff
+ echo ‘Function begin:’
Function begin:
+ local aaa=2000
+ echo aaa
aaa
+ echo bbb
bbb
+ echo ccc
ccc
+ echo aaa bbb ccc ddd eee fff
aaa bbb ccc ddd eee fff
+ echo aaa bbb ccc ddd eee fff
aaa bbb ccc ddd eee fff
+ echo 2000
2000
+ echo ‘Function end!’
Function end!
+ echo 111
111
+ echo 222
222
+ echo 333
333
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 111 222 333 444 555
111 222 333 444 555
+ echo 1000
1000
+ echo ‘Script end!’
Script end!1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
觀察整個執(zhí)行過程可以發(fā)現(xiàn),函數(shù)的參數(shù)適用方法跟腳本一樣,都可以使用n、*、$@這些符號來處理。而且函數(shù)參數(shù)跟函數(shù)內(nèi)部使用local定義的局部變量效果一樣,都是只在函數(shù)內(nèi)部能看到。函數(shù)外部看不到函數(shù)里定義的局部變量,當函數(shù)內(nèi)部的局部變量和外部的全局變量名字相同時,函數(shù)內(nèi)只能取到局部變量的值。當函數(shù)內(nèi)部沒有定義跟外部同名的局部變量的時候,函數(shù)內(nèi)部也可以看到全局變量。
bash編程支持遞歸調(diào)用函數(shù),跟其他編程語言不同的地方是,bash還可以遞歸的調(diào)用自身,這在某些編程場景下非常有用。我們先來看一個遞歸的簡單例子:
[zorro@zorrozou-pc0 bash]$ cat recurse.sh
#!/bin/bash
read_dir () {
for i in $1/*
do
if [ -d $i ]
then
read_dir $i
else
echo $i
fi
done
}
read_dir $11234567891011121314151617
這個腳本可以遍歷一個目錄下所有子目錄中的非目錄文件。關(guān)于遞歸,還有一個經(jīng)典的例子,fork炸彈:
。(){ 。|.& };.1
這一堆符號看上去很令人費解,我們來解釋一下每個符號的含義:根據(jù)函數(shù)的定義語法,我們知道。(){}的意思是,定義一個函數(shù)名子叫“。”。雖然系統(tǒng)中又個內(nèi)建命令也叫。,就是source命令,但是我們也知道,當函數(shù)和內(nèi)建命令名字沖突的時候,bash首先會將名字當成是函數(shù)來解釋。在{}包含的函數(shù)體中,使用了一個管道連接了兩個點,這里的第一個。就是函數(shù)的遞歸調(diào)用,我們也知道了使用管道的時候會打開一個subshell的子進程,所以在這里面就遞歸的打開了子進程。{}后面的分號只表示函數(shù)定義完畢的結(jié)束符,在之后就是調(diào)用函數(shù)名執(zhí)行的。,之后函數(shù)開始遞歸的打開自己,去產(chǎn)生子進程,直到系統(tǒng)崩潰為止。
bash并發(fā)編程和flock
在shell編程中,需要使用并發(fā)編程的場景并不多。我們倒是經(jīng)常會想要某個腳本不要同時出現(xiàn)多次同時執(zhí)行,比如放在crond中的某個周期任務,如果執(zhí)行時間較長以至于下次再調(diào)度的時間間隔,那么上一個還沒執(zhí)行完就可能又打開一個,這時我們會希望本次不用執(zhí)行。本質(zhì)上講,無論是只保證任何時候系統(tǒng)中只出現(xiàn)一個進程還是多個進程并發(fā),我們需要對進程進行類似的控制。因為并發(fā)的時候也會有可能產(chǎn)生競爭條件,導致程序出問題。
我們先來看如何寫一個并發(fā)的bash程序。在前文講到作業(yè)控制和wait命令使用的時候,我們就已經(jīng)寫了一個簡單的并發(fā)程序了,我們這次讓它變得復雜一點。我們寫一個bash腳本,創(chuàng)建一個計數(shù)文件,并將里面的值寫為0。然后打開100個子進程,每個進程都去讀取這個計數(shù)文件的當前值,并加1寫回去。如果程序執(zhí)行正確,最后里面的值應該是100,因為每個子進程都會累加一個1寫入文件,我們來試試:
[zorro@zorrozou-pc0 bash]$ cat racing.sh
#!/bin/bash
countfile=/tmp/count
if ! [ -f $countfile ]
then
echo 0 》 $countfile
fi
do_count () {
read count 《 $countfile
echo $((++count)) 》 $countfile
}
for i in `seq 1 100`
do
do_count &
done
wait
cat $countfile
rm $countfile12345678910111213141516171819202122232425
我們再來看看這個程序的執(zhí)行結(jié)果:
[zorro@zorrozou-pc0 bash]$ 。/racing.sh
26
[zorro@zorrozou-pc0 bash]$ 。/racing.sh
13
[zorro@zorrozou-pc0 bash]$ 。/racing.sh
34
[zorro@zorrozou-pc0 bash]$ 。/racing.sh
25
[zorro@zorrozou-pc0 bash]$ 。/racing.sh
45
[zorro@zorrozou-pc0 bash]$ 。/racing.sh
5123456789101112
多次執(zhí)行之后,每次得到的結(jié)果都不一樣,也沒有一次是正確的結(jié)果。這就是典型的競爭條件引起的問題。當多個進程并發(fā)的時候,如果使用的共享的資源,就有可能會造成這樣的問題。這里的競爭調(diào)教就是:當某一個進程讀出文件值為0,并加1,還沒寫回去的時候,如果有別的進程讀了文件,讀到的還是0。于是多個進程會寫1,以及其它的數(shù)字。解決共享文件的競爭問題的辦法是使用文件鎖。每個子進程在讀取文件之前先給文件加鎖,寫入之后解鎖,這樣臨界區(qū)代碼就可以互斥執(zhí)行了:
[zorro@zorrozou-pc0 bash]$ cat flock.sh
#!/bin/bash
countfile=/tmp/count
if ! [ -f $countfile ]
then
echo 0 》 $countfile
fi
do_count () {
exec 3《 $countfile
#對三號描述符加互斥鎖
flock -x 3
read -u 3 count
echo $((++count)) 》 $countfile
#解鎖
flock -u 3
#關(guān)閉描述符也會解鎖
exec 3》&-
}
for i in `seq 1 100`
do
do_count &
done
wait
cat $countfile
rm $countfile
[zorro@zorrozou-pc0 bash]$ 。/flock.sh
10012345678910111213141516171819202122232425262728293031323334
對臨界區(qū)代碼進行加鎖處理之后,程序執(zhí)行結(jié)果正確了。仔細思考一下程序之后就會發(fā)現(xiàn),這里所謂的臨界區(qū)代碼由加鎖前的并行,變成了加鎖后的串行。flock的默認行為是,如果文件之前沒被加鎖,則加鎖成功返回,如果已經(jīng)有人持有鎖,則加鎖行為會阻塞,直到成功加鎖。所以,我們也可以利用互斥鎖的這個特征,讓bash腳本不會重復執(zhí)行。
[zorro@zorrozou-pc0 bash]$ cat repeat.sh
#!/bin/bash
exec 3》 /tmp/.lock
if ! flock -xn 3
then
echo “already running!”
exit 1
fi
echo “running!”
sleep 30
echo “ending”
flock -u 3
exec 3》&-
rm /tmp/.lock
exit 01234567891011121314151617181920
-n參數(shù)可以讓flock命令以非阻塞方式探測一個文件是否已經(jīng)被加鎖,所以可以使用互斥鎖的特點保證腳本運行的唯一性。腳本退出的時候鎖會被釋放,所以這里可以不用顯式的使用flock解鎖。flock除了-u參數(shù)指定文件描述符鎖文件以外,還可以作為執(zhí)行命令的前綴使用。這種方式非常適合直接在crond中方式所要執(zhí)行的腳本重復執(zhí)行。如:
*/1 * * * * /usr/bin/flock -xn /tmp/script.lock -c ‘/home/bash/script.sh’1
關(guān)于flock的其它參數(shù),可以man flock找到說明。
受限bash
以受限模式執(zhí)行bash程序,有時候是很有必要的。這種模式可以保護我們的很多系統(tǒng)環(huán)境不受bash程序的誤操作影響。啟動受限模式的bash的方法是使用-r參數(shù),或者也可以rbash的進程名方式執(zhí)行bash。受限模式的bash和正常bash時間的差別是:
不能使用cd命令改變當前工作目錄。
不能改變SHELL、PATH、ENV和BASH_ENV環(huán)境變量。
不能調(diào)用含有/的命令路徑。
不能使用。執(zhí)行帶有/字符的命令路徑。
不能使用hash命令的-p參數(shù)指定一個帶斜杠\的參數(shù)。
不能在shell環(huán)境啟動的時候加載函數(shù)的定義。
不能檢查SHELLOPTS變量的內(nèi)容。
不能使用》, 》|, 《》, 》&, &》和 》》重定向操作符。
不能使用exec命令使用一個新程序替換當前執(zhí)行的bash進程。
enable內(nèi)建命令不能使用-f、-d參數(shù)。
不可以使用enable命令打開或者關(guān)閉內(nèi)建命令。
command命令不可以使用-p參數(shù)。
不能使用set +r或者set +o restricted命令關(guān)閉受限模式。
測試一個簡單的受限模式:
[zorro@zorrozou-pc0 bash]$ cat restricted.sh
#!/bin/bash
set -r
cd /tmp
[zorro@zorrozou-pc0 bash]$ 。/restricted.sh
。/restricted.sh: line 5: cd: restricted12345678
subshell
我們前面接觸過subshell的概念,我們之前說的是,當一個命令放在()中的時候,bash會打開一個子進程去執(zhí)行相關(guān)命令,這個子進程實際上是另一個bash環(huán)境,叫做subshell。當然包括放在()中執(zhí)行的命令,bash會在以下情況下打開一個subshell執(zhí)行命令:
使用&作為命令結(jié)束提交了作業(yè)控制任務時。
使用|連接的命令會在subshell中打開。
使用()封裝的命令。
使用coproc(bash 4.0版本之后支持)作為前綴執(zhí)行的命令。
要執(zhí)行的文件不存在或者文件存在但不具備可執(zhí)行權(quán)限的時候,這個執(zhí)行過程會打開一個subshell執(zhí)行。
在subshell中,有些事情需要注意。subshell中的$$取到的仍然是父進程bash的pid,如果想要取到subshell的pid,可以使用BASHPID變量:
[zorro@zorrozou-pc0 bash]$ echo $$ ;echo $BASHPID && (echo $$;echo $BASHPID)
5484
5484
5484
2458412345
可以使用BASH_SUBSHELL變量的值來檢查當前環(huán)境是不是在subshell中,這個值在非subshell中是0;每進入一層subshell就加1。
[zorro@zorrozou-pc0 bash]$ echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL))
0
1
21234
在subshell中做的任何操作都不會影響父進程的bash執(zhí)行環(huán)境。subshell除了PID和trap相關(guān)設置外,其他的環(huán)境都跟父進程是一樣的。subshell的trap設置跟父進程剛啟動的時候還沒做trap設置之前一樣。
協(xié)進程coprocess
在bash 4.0版本之后,為我們提供了一個coproc關(guān)鍵字可以支持協(xié)進程。協(xié)進程提供了一種可以上bash移步執(zhí)行另一個進程的工作模式,實際上跟作業(yè)控制類似。嚴格來說,bash的協(xié)進程就是使用作業(yè)控制作為實現(xiàn)手段來做的。它跟作業(yè)控制的區(qū)別僅僅在于,協(xié)進程的標準輸入和標準輸出都在調(diào)用協(xié)進程的bash中可以取到文件描述符,而作業(yè)控制進程的標準輸入和輸出都是直接指向終端的。我們來看看使用協(xié)進程的語法:
coproc [NAME] command [redirections]1
使用coproc作為前綴,后面加執(zhí)行的命令,可以將命令放到作業(yè)控制里執(zhí)行。并且在bash中可以通過一些方法查看到協(xié)進程的pid和使用它的輸入和輸出。例子:
zorro@zorrozou-pc0 bash]$ cat coproc.sh
#!/bin/bash
#例一:簡單命令使用
#簡單命令使用不能通過NAME指定協(xié)進程的名字,此時進程的名字統(tǒng)一為:COPROC。
coproc tail -3 /etc/passwd
echo $COPROC_PID
exec 0《&${COPROC[0]}-
cat
#例二:復雜命令使用
#此時可以使用NAME參數(shù)指定協(xié)進程名稱,并根據(jù)名稱產(chǎn)生的相關(guān)變量獲得協(xié)進程pid和描述符。
coproc _cat { tail -3 /etc/passwd; }
echo $_cat_PID
exec 0《&${_cat[0]}-
cat
#例三:更復雜的命令以及輸入輸出使用
#協(xié)進程的標準輸入描述符為:NAME[1],標準輸出描述符為:NAME[0]。
coproc print_username {
while read string
do
[ “$string” = “END” ] && break
echo $string | awk -F: ‘{print $1}’
done
}
echo “aaa:bbb:ccc” 1》&${print_username[1]}
echo ok
read -u ${print_username[0]} username
echo $username
cat /etc/passwd 》&${print_username[1]}
echo END 》&${print_username[1]}
while read -u ${print_username[0]} username
do
echo $username
done123456789101112131415161718192021222324252627282930313233343536373839404142
執(zhí)行結(jié)果:
[zorro@zorrozou-pc0 bash]$ 。/coproc.sh
31953
jerry:x:1001:1001::/home/jerry:/bin/bash
systemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologin
netdata:x:134:134::/var/cache/netdata:/bin/nologin
31955
jerry:x:1001:1001::/home/jerry:/bin/bash
systemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologin
netdata:x:134:134::/var/cache/netdata:/bin/nologin
ok
aaa
root
bin
daemon
ftp
http
uuidd
dbus
nobody
systemd-journal-gateway
systemd-timesync
systemd-network
systemd-bus-proxy
systemd-resolve
systemd-journal-remote
systemd-journal-upload
polkitd
avahi
colord
rtkit
gdm
usbmux
git
gnome-initial-setup
zorro
nvidia-persistenced
ntp
jerry
systemd-coredump
netdata1234567891011121314151617181920212223242526272829303132333435363738394041
最后
本文主要介紹了一些bash編程的常用技巧,主要包括的知識點為:
/dev/和/proc目錄的使用。
函數(shù)和遞歸。
并發(fā)編程和flock。
受限bash。
subshell。
協(xié)進程。
至此,我們的bash編程系列就算結(jié)束了。當然,shell其實到現(xiàn)在才剛剛開始。畢竟我們要真正實現(xiàn)有用的bash程序,還需要積累大量命令的使用。
評論