本文開始正式介紹shell腳本的編寫方法以及bash的語法。正則表達式
元字符
用來分隔詞(token)的單個字符,包括:shell
| & ; ( ) < > space tab
token
是指被shell當作一個單一單元的字符序列
bash中包含三種基本的token:保留關鍵字
,操做符
,單詞
。保留關鍵字
是指在shell中有明確含義的詞語,一般用來表達程序控制結構。包括:express
! case coproc do done elif else esac fi for function if in select then until while { } time [[ ]]
操做符
由一個或多個元字符
組成,其中控制操做符
包括:編程
|| & && ; ;; ( ) | |& <newline>
餘下的shell輸入均可以視爲普通的單詞
(word)。centos
shell腳本是指包含若干shell命令的文本文件,標準的bash腳本的第一行形如#!/bin/bash
,其中頂格寫的字符#!
向操做系統申明此文件是一個腳本,緊隨其後的/bin/bash
是此腳本程序的解釋器,解釋器能夠帶一個選項(選項通常是爲了對一些狀況作特殊處理,好比-x
表示開啓bash的調試模式)。
除首行外,其他行中以符號#
開頭的單詞及本行中此單詞以後的字符將做爲註釋,被解析器所忽略。數組
相比於其餘更正式的語言,bash的語法較爲簡單。大多數使用bash的人員,通常都先擁有其餘語言的語法基礎,在接觸bash的語法以後,會天然的將原有語法習慣套用到bash中來。事實上,bash的語法靈活多變,許多看起來像是固定格式的地方,實際上並非。這讓一些初學者以爲bash語法混亂不堪,複雜難記。這和bash的目的和使用者使用bash的目的有很大的關係,bash自己是爲了提供一個接口,來支持用戶經過命令與操做系統進行交互。用戶使用bash,通常是爲了完成某種系統管理的任務,而不是爲了作一款獨立的軟件。這些,都令人難以像學習其餘編程語言那樣對bash認真對待。其實,只要系統學習一遍bash語法以及一條命令的執行流程,就能夠說掌握了bash腳本編程的絕大多數內容。
bash語法只包括六種:簡單命令
、管道命令
、序列命令
、複合命令
、協進程命令
(bash版本4.0及以上)和函數定義
。bash
shell簡單命令(Simple Commands
)包括命令名稱,可選數目的參數和重定向(redirection
)。咱們在Linux基礎命令介紹系列裏所使用的絕大多數命令都是簡單命令。另外,在命令名稱前也能夠有若干個變量賦值語句(如上一篇所述,這些變量賦值將做爲命令的臨時環境變量被使用,後面有例子)。簡單命令以上述控制操做符
爲結尾。
shell命令執行後均有返回值
(會賦給特殊變量$?
),是範圍爲0-255的數字。返回值爲0,表示命令執行成功;非0,表示命令執行失敗。(可使用命令echo $?
來查看前一條命令是否執行成功)異步
管道命令(pipeline
)是指被|
或|&
分隔的一到多個命令。格式爲:socket
[time [-p]] [ ! ] command1 [ | command2 ... ]
其中保留關鍵字time
做用於管道命令表示當命令執行完成後輸出消耗時間(包括用戶態和內核態佔用時間),選項-p
能夠指定時間格式。
默認狀況下,管道命令的返回值是最後一個命令的返回值,爲0,表示true
,非0,則表示false
;當保留關鍵字!
做用於管道命令時,會對管道命令的返回值進行取反。
以前咱們介紹過管道的基本用法,表示將command1
的標準輸出經過管道鏈接至command2
的標準輸入,這個鏈接要先於命令的其餘重定向操做(試對比>/dev/null 2>&1
和2>&1 >/dev/null
的區別)。若是使用|&
,則表示將command1
的標準輸出和標準錯誤都鏈接至管道。
管道兩側的命令均在子shell(subshell
)中執行,這裏須要注意:在子shell中對變量進行賦值時,父shell是不可見的。編程語言
#例如 [root@centos7 ~]# echo 12345|read NUM [root@centos7 ~]# echo $NUM #因爲echo和read命令均在子shell中執行,因此當執行完畢時,在父shell中輸出變量的值爲空 [root@centos7 ~]#
序列命令(list
)是指被控制操做符;
,&
,&&
或||
分隔的一到多個管道命令,以;
、&
或<newline>
爲結束。
在這些控制操做符中,&&
和||
有相同的優先級,而後是;
和&
(也是相同的優先級)。
若是命令以&
爲結尾,此命令會在一個子shell中後臺執行,當前shell不會等待此命令執行結束,而且不論它是否執行成功,其返回值均爲0。
以符號;
分隔的命令按順序執行(和換行符的做用幾乎相同),shell等待每一個命令執行完成,它們的返回值是最後一個命令的返回值。
以符號&&
和||
鏈接的兩個命令存在邏輯關係。command1 && command2
:先執行command1,當且僅當command1的返回值爲0,才執行command2。command1 || command2
:先執行command1,當且僅當command1的返回值非0,才執行command2。
腳本舉例:
#!/bin/bash #簡單命令 echo $PATH > file #管道命令 cat file|tr ':' ' ' #序列命令 IFS=':' read -a ARRAY <file && echo ${ARRAY[4]} || echo "賦值失敗" echo "命令返回值爲:$?。" #驗證變量的臨時做用域 echo "$IFS"|sed 'N;s/[ \t\n]/-/g'
執行結果(在腳本所在目錄直接執行./test.sh
):
[root@centos7 ~]# ./test.sh /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /root/bin /root/bin 命令返回值爲:0。 --- [root@centos7 ~]#
注意例子中序列命令
的寫法,其中IFS=':'
只臨時對內置命令read起做用(做爲單詞分隔符來分隔read的輸入),read命令結束後,IFS又恢復到原來的值:$' \d\n'。&&
和||
在這裏相似於分支語句,read命令執行成功則執行輸出數組的第五個元素,不然執行輸出"賦值失敗"。
一、(list)
list
將在subshell
中執行(注意賦值語句和內置命令修改shell狀態不能影響當父shell),返回值是list的返回值。
此複合命令前若是使用擴展符$
,shell稱之爲命令替換
(另外一種寫法爲`list`)。shell會把命令的輸出做爲命令替換
擴展以後的結果使用。命令替換
能夠嵌套。
二、{ list; }
list
將在當前shell環境中執行,必須以換行或分號爲結尾(即便只有一個命令)。注意不一樣於shell元字符
:(
和)
,{
和}
是shell的保留關鍵字
,由於保留關鍵字不能分隔單詞,因此它們和list
之間必須有空白字符或其餘shell元字符。
三、((expression))
expression
是數學表達式(相似C語言的數學表達式),若是表達式的值非0,則此複合命令的返回值爲0;若是表達式的值爲0,則此複合命令的返回值爲1。
此種複合命令和使用內置命令let "expression"
是同樣的。
數學表達式中支持以下操做符,操做符的優先級,結合性,計算方法都和C語言一致(按優先級從上到下遞減排列):
id++ id-- # 變量後自加 後自減 ++id --id # 變量前自加 前自減 - + # 一元減 一元加 ! ~ # 邏輯否認 位否認 ** # 乘方 * / % # 乘 除 取餘 + - # 加 減 << >> # 左位移 右位移 <= >= < > # 比較大小 == != # 等於 不等於 & # 按位與 ^ # 按位異或 | # 按位或 && # 邏輯與 || # 邏輯或 expr?expr:expr # 條件表達式 = *= /= %= += -= <<= >>= &= ^= |= # 賦值表達式 expr1 , expr2 # 逗號表達式
在數學表達式中,可使用變量做爲操做數,變量擴展
要先於表達式的求值。變量還能夠省略擴展符號$
,若是變量的值爲空或非數字和運算符的其餘字符串,將使用0代替它的值作數學運算。
以0
開頭的數字將被解釋爲八進制數,以0x
或0X
開頭的數字將被解釋爲十六進制數。其餘狀況下,數字的格式能夠是[base#]n
。可選的base#
表示後面的數字n
是以base(範圍是2-64)爲基的數字,如2#11011
表示11011是一個二進制數字,命令((2#11011))
的做用會使二進制數轉化爲十進制數。若是base#
省略,則表示數字以10爲基。
複合命令((expression))
並不會輸出表達式的結果,若是須要獲得結果,需使用擴展符$
表示數學擴展
(另外一種寫法爲$[expression]
)。數學擴展
也能夠嵌套。
括號()
能夠改變表達式的優先級。
腳本舉例:
#!/bin/bash # (list) (ls|wc -l) #命令替換並賦值給數組 注意區分數組賦值array=(...)和命令替換$(...) array=($(seq 10 10 $(ls|wc -l) | sed -z 's/\n/ /g')) #數組取值 echo "${array[*]}" # { list; } #將文件file1中的第一行寫入file2,{ list; } 是一個總體。 { read line;echo $line;} >file2 <file1 #數學擴展 A=$(wc -c file2 |cut -b1) #此時變量A的值爲5 B=4 echo $((A+B)) echo $(((A*B)**2)) #賦值並輸出 echo $((A|=$B)) #條件運算符 此命令意爲:判斷表達式A>=7是否爲真,若是爲真則計算A-1,不然計算(B<<1)+3。而後將返回結果與A做異或運算並賦值給A。 ((A^=A>=7?A-1:(B<<1)+3)) echo $A
執行結果:
[root@centos7 temp]# ./test.sh 43 10 20 30 40 9 400 5 14
四、[[ expression ]]
此處的expression
是條件表達式(並不是數學擴展中的條件表達式)。此種命令的返回值取決於條件表達式
的結果,結果爲true
,則返回值爲0,結果爲false
,則返回值爲1。條件表達式
除能夠用在複合命令中外,還能夠用於內置命令test
和[
,因爲test
、[[
、]]
、[
和]
是內置命令或保留關鍵字,因此同保留關鍵字{
和}
同樣,它們與表達式之間都要有空格或其餘shell元字符。
條件表達式的格式包括:
-b file #判斷文件是否爲塊設備文件 -c file #判斷文件是否爲字符設備文件 -d file #判斷文件是否爲目錄 -e file #判斷文件是否存在 -f file #判斷文件是否爲普通文件 -g file #判斷文件是否設置了SGID -h file #判斷文件是否爲符號連接 -p file #判斷文件是否爲命名管道文件 -r file #判斷文件是否可讀 -s file #判斷文件是否存在且內容不爲空(也能夠是目錄) -t fd #判斷文件描述符fd是否開啓且指向終端 -u file #判斷文件是否設置SUID -w file #判斷文件是否可寫 -x file #判斷文件是否可執行 -S file #判斷文件是否爲socket文件 file1 -nt file2 #判斷文件file1是否比file2更新(根據mtime),或者判斷file1存在但file2不存在 file1 -ot file2 #判斷文件file1是否比file2更舊,或者判斷file2存在但file1不存在 file1 -ef file2 #判斷文件file1和file2是否互爲硬連接 -v name #判斷變量狀態是否爲set(見上一篇) -z string #判斷字符串是否爲空 -n string #判斷字符串是否非空 string1 == string2 #判斷字符串是否相等 string1 = string2 #判斷字符串是否相等 string1 != string2 #判斷字符串是否不相等 string1 < string2 #判斷字符串string1是否小於字符串string2(字典排序),用於內置命令test中時,小於號須要轉義:\< string1 > string2 #判斷字符串string1是否大於字符串string2(字典排序),用於內置命令test中時,大於號須要轉義:\> NUM1 -eq NUM2 #判斷數字是否相等 NUM1 -ne NUM2 #判斷數字是否不相等 NUM1 -lt NUM2 #判斷數字NUM1是否小於數字NUM2 NUM1 -le NUM2 #判斷數字NUM1是否小於等於數字NUM2 NUM1 -gt NUM2 #判斷數字NUM1是否大於數字NUM2 NUM1 -ge NUM2 #判斷數字NUM1是否大於等於數字NUM2
[[ expr ]]
和[ expr ]
(test expr
是[ expr ]
的另外一種寫法,效果相同)還接受以下操做符(從上到下優先級遞減):
! expr #表示對錶達式expr取反 ( expr ) #表示提升expr的優先級 expr1 -a expr2 #表示對兩個表達式進行邏輯與操做,只能用於 [ expr ] 和 test expr 中 expr1 && expr2 #表示對兩個表達式進行邏輯與操做,只能用於 [[ expr ]] 中 expr1 -o expr2 #表示對兩個表達式進行邏輯或操做,只能用於 [ expr ] 和 test expr 中 expr1 || expr2 #表示對兩個表達式進行邏輯或操做,只能用於 [[ expr ]] 中
在使用操做符==
和!=
判斷字符串是否相等時,在[[ expr ]]
中等號右邊的string2能夠被視爲模式匹配string1,規則和通配符匹配一致。([ expr ]
不支持)[[ expr ]]
中比較兩個字符串時還能夠用操做符=~
,符號右邊的string2能夠被視爲是正則表達式匹配string1,若是匹配,返回真,不然返回假。
五、if list; then list; [ elif list; then list; ] ... [ else list; ] fi
條件分支命令。首先判斷if後面的list的返回值,若是爲0,則執行then後面的list;若是非0,則繼續判斷elif後面的list的返回值,若是爲0,則......,若返回值均非0,則最終執行else後的list。fi是條件分支的結束詞。
注意這裏的list均是命令,因爲要判斷返回值,一般使用上述條件表達式
來進行判斷
形如:
if [ expr ] then list elif [ expr ] then list ... else list fi
甚至,許多人認爲這樣就是if語句的固定格式,其實if後面能夠是任何shell命令,只要可以判斷此命令的返回值。如:
[root@centos7 ~]# if bash;then echo true;else echo false;fi [root@centos7 ~]# #執行後沒有任何輸出 [root@centos7 ~]# exit exit true #因爲執行了bash命令開啓了一個新的shell,因此執行exit以後if語句纔得到返回值,並作了判斷和輸出 [root@centos7 ~]#
腳本舉例:
#!/bin/bash #條件表達式 declare A #判斷變量A是否set [[ -v A ]] && echo "var A is set" || echo "var A is unset" #判斷變量A的值是否爲空 [ ! $A ] && echo false || echo true test -z $A && echo "var A is empty" #通配與正則 A="1234567890abcdeABCDE" B='[0-9]*' C='[0-9]{10}\w+' [[ $A = $B ]] && echo '變量A匹配通配符[0-9]*' || echo '變量A不匹配通配符[0-9]*' [ $A == $B ] && echo '[ expr ]中可以使用通配符' || echo '[ expr ]中不能使用通配符' [[ $A =~ $C ]] && echo '變量A匹配正則[0-9]{10}\w+' || echo '變量A不匹配正則[0-9]{10}\w+' #if語句 # 此例並無什麼特殊的意義,只爲說明幾點須要注意的地方: # 一、if後面能夠是任何可以判斷返回值的命令 # 二、直接執行復合命令((...))沒有輸出,要取得表達式的值必須經過數學擴展 $((...)) # 三、複合命令((...))中表達式的值非0,返回值纔是0 number=1 if if test -n $A then ((number+1)) else ((number-1)) fi then echo "數學表達式值非0,返回值爲0" else echo "數學表達式值爲0,返回值非0" fi # if語句和控制操做符 && || 鏈接的命令很是類似,但要注意它們之間細微的差異: # if語句中then後面的命令不會影響else後的命令的執行 # 但&&後的命令會影響||後的命令的執行 echo '---------------' if [[ -r file && ! -d file ]];then grep -q hello file else awk '/world/' file fi echo '---------------' # 上面的if語句無輸出,但下面的命令有輸出 [ -r file -a ! -d file ] && grep -q hello file || awk '/world/' file # 能夠將控制操做符鏈接的命令寫成這樣來忽略&&後命令的影響(使用了內置命令true來返回真): echo '---------------' [ -r file -a ! -d file ] && (grep -q hello file;true) || awk '/world/' file
六、for name [[ in [ word ... ] ];]do list;done
七、for ((expr1;expr2;expr3));do list;done
bash中的for循環語句支持如上兩種格式,在第一種格式中,先將in後面的word
進行擴展,而後將獲得的單詞列表逐一賦值給變量name
,每一次賦值都執行一次do後面的list
,直到列表爲空。若是in word
被省略,則將位置變量逐一賦值給name並執行list。第二種格式中,雙圓括號內都是數學表達式
,先計算expr1,而後反覆計算expr2,直到其值爲0。每一次計算expr2獲得非0值,執行do後面的list和第三個表達式expr3。若是任何一個表達式省略,則表示其值爲1。for語句的返回值是執行最後一個list的返回值。
腳本舉例:
#!/bin/bash # word舉例 for i in ${a:=3} $(head -1 /etc/passwd) $((a+=2)) do echo -n "$i " done echo $a # 省略 in word declare -a array for number do array+=($number) done echo ${array[@]} # 數學表達式格式 for((i=0;i<${#array[*]};i++)) do echo -n "${array[$i]} "|sed 'y/1234567890/abcdefghij/' done;echo
執行:
[root@centos7 temp]# ./test.sh "$(seq 10)" # 注意此處"$(seq 10)"將做爲一個總體賦值給$1,若是去掉雙引號將會擴展成10個值並賦給 $1 $2 ... ${10} 3 root:x:0:0:root:/root:/bin/bash 5 5 # 是否帶雙引號並不影響執行結果,隻影響第二個for語句的循環次數。 1 2 3 4 5 6 7 8 9 10 a b c d e f g h i aj [root@centos7 temp]#
八、while list-1; do list-2; done
九、until list-1; do list-2; done
while命令會重複執行list-2,只要list-1的返回值爲0;until命令會重複執行list-2,只要list-1的返回值爲非0。while和until命令的返回值是最後一次執行list-2的返回值。break
和continue
兩個內置命令能夠用於for、while、until循環中,分別表示跳出循環和中止本次循環開始下一次循環。
十、case word in [[(] pattern [ | pattern]...) list ;;] ... esac
case命令會將word
擴展後的值和in後面的多個不一樣的pattern
進行匹配(通配符匹配),若是匹配成功則執行相應的list
。
list後使用操做符;;
時,表示若是執行了本次的list,那麼將再也不進行下一次的匹配,case命令結束;
使用操做符;&
,則表示執行完本次list後,再執行緊隨其後的下一個list(不判斷是否匹配);
使用操做符;;&
,則表示繼續下一次的匹配,若是匹配成功,那麼執行相應的list。
case命令的返回值是執行最後一個命令的返回值,當匹配均沒有成功時,返回值爲0。
腳本舉例:
#!/bin/bash # while unset i j while ((i++<$(grep -c '^processor' /proc/cpuinfo))) do #每一個後臺運行的yes命令將佔滿一核CPU yes >/dev/null & done # ------------------------------------------------- # until # 獲取yes進程PID數組 PIDS=($(ps -eo pid,comm|grep -oP '\d+(?= yes$)')) # 逐個殺掉yes進程 until ! ((${#PIDS[*]}-j++)) do kill ${PIDS[$j-1]} done # ------------------------------------------------- # case user_define_command &>/dev/null case $? in 0) echo "執行成功" ;; 1) echo "未知錯誤" ;; 2) echo "誤用shell命令" ;; 126) echo "權限不夠" ;; 127) echo "未找到命令" ;; 130) echo "CTRL+C終止" ;; *) echo "其餘錯誤" ;; esac # ------------------------------------------------- #定義數組 c=(1 2 3 4 5) #關於各類複合命令結合使用的例子: echo -e "$( for i in ${c[@]} do case $i in (1|2|3) printf "%d\n" $((i+1)) ;; (4|5) printf "%d\n" $((i-1)) ;; esac done )" | while read NUM do if [[ $NUM -ge 4 ]];then printf "%s\n" "數字${NUM}大於等於4" else printf "%s\n" "數字${NUM}小於4" fi done
執行結果:
[root@centos7 temp]# ./test.sh ./test.sh: 行 18: 18671 已終止 yes > /dev/null ./test.sh: 行 18: 18673 已終止 yes > /dev/null ./test.sh: 行 18: 18675 已終止 yes > /dev/null ./test.sh: 行 18: 18677 已終止 yes > /dev/null ./test.sh: 行 18: 18679 已終止 yes > /dev/null ./test.sh: 行 18: 18681 已終止 yes > /dev/null ./test.sh: 行 20: 18683 已終止 yes > /dev/null ./test.sh: 行 20: 18685 已終止 yes > /dev/null 未找到命令 數字2小於4 數字3小於4 數字4大於等於4 數字3小於4 數字4大於等於4 [root@centos7 temp]#
十一、select name [ in word ] ; do list ; done
select命令適用於交互式菜單選擇場景。word
的擴展結果組成一系列可選項供用戶選擇,用戶經過鍵入提示字符中可選項前的數字來選擇特定項目,而後執行list
,完成後繼續下一輪選擇,須要使用內置命令break
來跳出循環。
腳本舉例:
#!/bin/bash echo "系統信息:" select item in "host_name" "user_name" "shell_name" "quit" do case $item in host*) hostname;; user*) echo $USER;; shell*) echo $SHELL;; quit) break;; esac done
執行結果:
[root@centos7 ~]# ./test.sh 系統信息: 1) host_name 2) user_name 3) shell_name 4) quit #? 1 centos7 #? 2 root #? 3 /bin/bash #? 4 [root@centos7 ~]#
協進程命令是指由保留關鍵字coproc
執行的命令(bash4.0版本以上),其命令格式爲:
coproc [NAME] command [redirections]
命令command
在子shell中異步執行,就像被控制操做符&
做用而放到了後臺執行,同時創建起一個雙向管道,鏈接該命令和當前shell。
執行此命令,即建立了一個協進程,若是NAME省略(command
爲簡單命令時必須省略,此時使用默認名COPROC
),則稱爲匿名協進程
,不然稱爲命名協進程
。
此命令執行時,command
的標準輸出和標準輸入經過雙向管道分別鏈接到當前shell的兩個文件描述符,而後文件描述符又分別賦值給了數組元素NAME[0]
和NAME[1]
。此雙向管道的創建要早於命令command
的其餘重定向操做。被鏈接的文件描述符能夠當成變量來使用。子shell的pid能夠經過變量NAME_PID來得到。
關於協進程的例子,咱們在下一篇給出。
bash函數定義的格式有兩種:
name () compound-command [redirection] function name [()] compound-command [redirection]
這樣定義了名爲name的函數,使用保留關鍵字function定義函數時,括號能夠省略。函數的代碼塊能夠是任意一個上述的複合命令
(compound-command)。
腳本舉例:
#!/bin/bash #經常使用定義方法: func_1() { #局部變量 local num=6 #嵌套執行函數 func_2 #函數的return值保存在特殊變量?中 if [ $? -gt 10 ];then echo "大於10" else echo "小於等於10" fi } ################ func_2() { # 內置命令return使函數退出,並使其的返回值爲命令後的數字 # 若是return後沒有參數,則返回函數中最後一個命令的返回值 return $((num+5)) } #執行。就如同執行一個簡單命令。函數必須先定義後執行(包括嵌套執行的函數) func_1 ############### #通常定義方法 #函數名後面能夠是任何複合命令: func_3() for NUM do # 內置命令shift將會調整位置變量,每次執行都把前n個參數撤銷,後面的參數前移。 # 若是shift後的數字省略,則表示撤銷第一個參數$1,其後參數前移($2變爲$1....) shift echo -n "$((NUM+$#)) " done #函數內部位置變量被重置爲函數的參數 func_3 `seq 10`;echo
執行結果:
[root@centos7 temp]# ./test.sh 大於10 10 10 10 10 10 10 10 10 10 10 [root@centos7 temp]#
這些就是bash的全部命令語法。bash中任何複雜難懂的語句都是這些命令的變化組合。