SHELL(bash)腳本編程二:語法

本文開始正式介紹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>&12>&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開頭的數字將被解釋爲八進制數,以0x0X開頭的數字將被解釋爲十六進制數。其餘狀況下,數字的格式能夠是[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的返回值。
breakcontinue兩個內置命令能夠用於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中任何複雜難懂的語句都是這些命令的變化組合。

相關文章
相關標籤/搜索