在Linux系統使用過程中,不可避免的要編寫腳本,如進行大量重複操作,或需要根據條件自動執行某操作。本文介紹了bash腳本編寫的相關內容,文末有一些示例。
文章目錄
一、bash腳本介紹
1. shell
嚴格來講,這裏的bash是工作在用戶空間的一個程序而已,與其他程序不同的是,該程序負責與使用者交互,可以說是計算機與用戶的溝通的媒介。
我們通過輸入設備輸入指令或數據,有它負責進行相應操作,或啓動另一個進程處理。從這個意義來講,他就像是一個“外殼”,普通用戶與計算機的交互都是通過該程序。這樣的程序我們將其稱爲Shell,Windows平臺如桌面,cmd,powershell,Linux平臺有bash,zsh,csh,KDE,Gnome等。
2. bash腳本
作爲一款shell程序,bash的強大之處在於,其內部支持其特有的命令輸入方式,如,可以將多個命令寫在一行,中間使用分號分隔即可;再如,它可以進行條件判斷等1。
這裏要說明的其另一個重要特性,他可以將以文本文件的內容,按照其規則進行解釋後執行,效果如用戶鍵入的相同。該規則可以稱爲bash的語法,該文件可稱爲bash腳本。
由於其這種特性,它幾乎可以被當做一門編程語言。
二、bash腳本基礎
1. 編程語言分類
爲了從頭講明,也便於同其他語言對比,這裏提一下這個話題。關於軟件編程,這裏有基礎介紹:https://blog.csdn.net/xiyangyang410/article/details/85043737#2__46
這裏我們只討論高級語言,我們知道,開發人員寫的代碼最初爲文本文件,而其需要被“翻譯”爲計算機能識別的指令才能執行。此處可從代碼的“翻譯”方式對於編程語言做以簡要說明:
-
- 編譯型語言
- 代碼需要由“翻譯”工具翻譯爲二進制格式,在進行該操作之前會進行一系列檢查,檢查全部通過纔會繼續。
- 該“翻譯”工具被叫做編譯器(Compiler),翻譯過程被稱爲編譯(Compilation)
- 這樣的編程語言如C、C++、Pascal等,而這類軟件的構建過程一般還有彙編與鏈接等過程
-
- 解釋型語言
- 解釋型語言的翻譯工具被稱爲解釋器(Interpreter),這類語言開發的程序運行時,需要事先啓動一個解釋器,先進行基本的語法檢查,無誤後由解釋器逐句解釋代碼執行
- 這種編程語言只需事先安裝對應平臺的解釋器即可,遂其可以做到較編譯型語言好的跨平臺性
-
- 半解釋型語言
- 由於解釋型語言的執行特點,其運行速度自然不比編譯型語言快,而半解釋型語言兼顧了以上二者的有點,有的地方也稱之爲半編譯型語言。而他的“翻譯”方式爲,先將文本代碼編譯爲字節碼,該文件爲二進制,但是不能直接執行,需要相應的解釋器解釋執行。如Java、Python等
一般的,我們將解釋型語言的源代碼文件叫做腳本,有時也將半解釋型語言代碼文件這麼稱呼,但是一般編譯型語言的源代碼文件不這麼叫。
比如你可能聽過bat腳本、vb腳本,甚至Python腳本,但是你聽過C腳本嗎?
2. 編寫代碼的約定
幾乎所有的編程語言,在代碼編寫過程中都會有一些約定俗成的規則,並且也有很多團隊有自己內部的開發規範,這裏對基本的規則予以介紹,以避免養成一些陋習。
-
- 變量名要做到見名知意
- 不要出現類似a,b,c,或data1,data2之類的變量名
- 拼音並不能很好的做到見名知意,應使用英語
- 可使用駝峯法,如PeopleCount,或使用下劃線分隔單詞,如people_count
-
- 代碼需要有註釋
- 對於某些複雜邏輯,或一些標識值的意義等,應該在註釋部分做以說明
- 在代碼首部,應該有對該代碼文件的相關說明,如代碼的功能介紹、開發日期、作者,有的甚至需要註明版本變更與內容
三、bash語法介紹
bash可以說是最好學的編程語言了,因爲他在工作時是直接調用的系統命令。而其他編程語言,我們將其本身的語法學完之後,很難直接使用它快速完成實際工作——我們還需要學一大堆的庫。
bash編程,只要瞭解相關命令的用法,以及bash的語法即可。
1. 變量
變量(Variable) 相信大家並不陌生,其實質上就是一段命名的內存空間。而在bash中,同樣支持變量的概念。
bash中的變量類型
變量類型 | 說明 |
---|---|
環境變量 | 作用域爲當前shell進程及其子進程 |
本地變量 | 作用域爲整個bash進程 |
局部變量 | 作用域爲當前代碼段(通常指函數) |
位置變量 | $1,$2,…用於讓腳本在腳本代碼中調用通過命令行傳遞給它的參數 |
特殊變量 | 保存某些特殊數據 |
- 特殊變量
$0
: 腳本名稱本身$?
: 上一條命令的執行狀態- 狀態用數字表示:0-255
- 成功:0
- 失敗:1-255
$$
:腳本運行的當前進程ID$!
:Shell最後運行的後臺進程的PID$#
: 參數數量$*
: 所有參數的一個字符串$@
:所有參數單獨作爲每個字符串$1
、$2
…:位置變量,對應第一、第二個參數- 關於$@與$*的區別:
- 只有在雙引號中體現出來。假設在腳本運行時寫了三個參數(分別存儲在
$1
$2
$3
)則"$*
" 等價於 “$1 $2 $3
"(傳遞了一個參數);而“$@
" 等價於 “$1
” “$2
” “$3
”(傳遞了三個參數)
變量聲明
bash中變量的命名規則同其他語言類似,變量名只能包含數字、字母和下劃線,而且不能以數字開頭,關鍵字不能用作變量名,另外,變量賦值時不能使用$。
本地變量的聲明
bash爲弱類型語言,故在聲明變量時不必指定變量類型(但這不代表這些變量沒有類型),可直接使用如下形式聲明變量:
[set] VARNAME=VALUE
set可省略
使用不帶參數的set
命令可查看系統已定義的所有變量
只讀變量的聲明
readonly VARNAME
或
declare -r VARNAME
在聲明時指定readonly
,或使用declare -r
,可聲明只讀變量(常量),由於其在聲明後不可更改內容,需要在聲明時進行初始化。
環境變量的聲明
export VARNAME=VALUE
或
VARNAME=VALUE; export VARNAME
或
VARNAME=VALUE; declare -x VARNAME
或
declare -x VARNAME=VALUE
- Tips;
- 在同一行執行的多條命令中間可使用;(分號)隔開
- 環境變量的查看
- printenv
- env
- export
- declare -x
環境變量對當前shell及其子shell都有效
局部變量的聲明
local VARNAME=VALUE
僅對局部代碼生效
數組的聲明
所謂數組,是有序的元素序列。若將有限個類型相同的變量的集合命名,那麼這個名稱爲數組名。它使用連續的內存空間,可以使用索引來獲取相關元素。
在bash-4及以後的版本中,支持關聯數組(Associative Array) 的概念,即可自定義索引,而不是僅僅以0,1,2……爲其索引的索引數組(Indexed Array),類似於Python中字典(dict) 的概念。bash中聲明數組的方式爲:
declare -a ARRAY_NAME
# 聲明一個索引數組
declare -A ARRAY_NAME
# 聲明一個關聯數組
數組的初始化或賦值
在聲明一個數組後,可對其進行初始化,使用ARRAY_NAME=("VAR1" "VAR2" "VAR3")
的形式,各元素之間使用空白分隔,bash將按給出的次序給予每一個元素索引(0,1,2……),若數組聲明爲關聯數組,可使用ArrayName=([INDEX]='VAR' [INDEX]='VAR')
的形式,此處的INDEX
並非必須是數字。
可以使用如下方式對數組的某個元素進行賦值:
ARRAY_NAME[INDEX]=VAR
注意,此處沒有$
,關於bash中數組的其他用法,下文將做介紹
變量的引用
- ${VARNAME} 不會引起混淆的話,{}可省略
- “” 弱引用,其中的變量引用會被替換爲變量值
- ‘’ 強引用,其中的變量引用不會被替換爲變量值,而保持原字符串
數組的引用
關於數組的引用,可使用如下格式:
${ARRAY_NAME[INDEX]}
注意,此處的{}
(花括號)不能省略,而數組名將引用數組首元素
特殊引用
變量的賦值可使用VAR_NAME=VALUE
的形式,此處VALUE
可以爲字面值,也可以引用變量,此處介紹以下特殊引用方式
-
${var:-VALUE}
- 若
var
變量爲空,或未設置,則返回VALUE
,否則返回var
變量的值
-
${var:=VALUE}
- 若
var
變量爲空,或未設置,則返回VALUE
,並將VALUE
賦值給var變量,否則返回var
變量的值
-
${var:+VALUE}
- 若
var
變量不空,或未設置,則返回VALUE
,否則返回空,與${var:-VALUE}
相反
-
${var:?ERROR_INFO}
- 若
var
爲空,或未設置,那麼返回ERROR_INFO
爲錯誤提示,否則返回var
的值
變量的撤銷
變量將在當前shell的聲明週期結束時被自動撤銷,而欲手動撤銷變量,可使用unset VAR_NAME
,此時,若VAR_NAME
爲變量或數組,則可直接撤銷,若爲數組的某元素,則可僅撤銷該元素
2. 邏輯運算
與(AND)
“與”表示“並且”之意,即兩種事件都發生,使用 && 表示,其運算結果爲
- 1 && 1 = 1
- 1 && 0 = 0
- 0 && 1 = 0
- 0 && 0 = 0
即二者都爲真(True),結果才爲真(False),否則爲假
或(OR)
“或”表示二者任一即可,使用 || 表示,其運算結果爲
- 1 || 1 = 1
- 1 || 0 = 1
- 0 || 1 = 1
- 0 || 0 = 0
即二者都爲假,結果才爲假,否則爲真
非(NOT)
“非”表示“不”,取相反結果之意,這是一個單目運算符,使用 ! 其運算結果爲
- ! 1 = 0
- ! 0 = 1
邏輯短路
概念
由於“與”和“或”運算時自左而右的,加之其運算特性,我們可以得出如下結論
- 若且(AND)運算的第一個運算數(Operand)爲假,結果一定爲假;
- 若或(OR)運算的第一個運算數爲真,結果一定爲真;
如此則不必再去計算其二個運算數(或表達式)的結果,可直接得出最終結果,這種運算方式成爲短路運算,也叫作邏輯短路。
應用
通過這種特性,在bash中可是實現類似if條件判斷的處理。
我們知道,程序在執行完後會返回一個執行狀態,用於標識程序執行成功與否,使用0標識執行成功(即“真”),非0則標識執行失敗(即“假”)
可通過判斷該狀態返回值,對不同的結果(成功或失敗)處以不同的操作,如
查看user1是否存在,若存在,則輸出其信息,否則創建之:
id user1 2> /dev/null || useradd user1
可以組合多個這樣的命令以實現更復雜的控制
例
- 如果用戶
user1
存在,就係顯示用戶已存在,否則就添加此用戶id user1 && echo "user1 exists." || useradd user1
- 如果用戶
user1
不存在,就添加此用戶,否則就顯示用戶已存在! id user1 && useradd user1 || echo "user1 exists." # 或 id user1 || useradd user1 && echo "user1 exists."
- 如果用戶
user1
不存在,就添加並且給密碼,否則顯示其已存在! id user1 && useradd user1 && echo "user1" | passwd --stdin user1 || echo "user1 exists."
德摩根定律
該定律應用很廣泛,此處也將其列出
3. 條件分支
if
對於以上的根據不同條件進行不同操作的方式,可使用一個更易讀的語法:if語句
單分支if
表示,如果某條件滿足,則執行特性操作,否則不行該操作,其使用格式爲
if BOOL_EXP; then
STATEMENT
fi
BOOL_EXP
爲一個布爾表達式,可以是一個命令,此時將取命令的執行狀態返回值作爲布爾值參與表達式計算
若BOOL_EXP
值爲真,則執行代碼塊中內容(STATEMENT
),否則不執行
then
關鍵字也可以另起一行,另外對於bash而言,縮進不是必要的,但爲了代碼易讀,建議對代碼塊中的語句使用縮進。而有的編程語言這一要求是必須的,如Python
雙分支if
if BOOL_EXP; then
STATEMENT1
else
STATEMENT2
fi
表示若BOOL_EXP
爲真,執行STATEMENT1
,否則執行STATEMENT2
多分支if
if BOOL_EXP1; then
STATEMENT1
elif BOOL_EXP2; then
STATEMENT2
elif BOOL_EXP3; then
STATEMENT3
...
else
STATEMENT4
fi
表示若BOOL_EXP1
爲真,執行STATEMENT1
,若BOOL_EXP2
位真,執行STATEMENT2…
elif
段可以出現多次,最後的else
也是可選的,若有,表示若所有條件都不滿足,執行STATEMENT4
故以上的代碼使用if語句的實現爲
if ! id user1 2> /dev/null; then
useradd user1
fi
case
有時在處理有多種條件分支的情況時,若使用if語句,我們不得不編寫冗長的elif段,此時,可以使用case語句來處理該情形,case語句的基本使用格式爲
case EXPRESSION in
PATTERN1)
STATEMENTS
;;
PATTERN2)
STATEMENTS
;;
...
esac
PATTERN爲過濾的模式,其支持的方式爲
- | 或,如PATTERN1|PATTERN2表示PATTERN1或PATTERN2均滿足條件
- * 匹配任意長度的任意字符
- ? 匹配任意單個字符
- [] 匹配範圍
如
#!/bin/bash
case $1 in
'start')
echo "start server...";;
'stop')
echo "stop server...";;
'restart')
echo "restarting server...";;
'status')
echo "running...";;
*)
echo "`basename $0` {start|stop|restart|ststus}";;
esac
4. 算術運算
由於bash是弱類型的語言,其聲明的變量默認爲字符,運算法則默認也是按照字符運算進行的,若要使其進程算術運算,可使用如下幾種方式:
- let VAR = ARITH_EXPR
- VAR = $[ARITH_EXPR]
- VAR = $((ARITH_EXPR))
- VAR = $(expr ARITH_EXPR)
bash中常用的運算符如下
運算符 | 描述 |
---|---|
+ | 加 |
- | 減 |
* | 乘 |
/ | 除 |
** | 乘方 |
% | 取模 |
若欲計算變量A與變量B的和,以上表示的實現爲:
#let VAR = ARITH_EXPR
let C = $A + $B
#VAR = $[ARITH_EXPR]
C = $[$A + $B]
#VAR = $((ARITH_EXPR))
C = $(($A + $B))
#VAR = $(ARITH_EXPR)
C = $(expr $A + $B)
C = `expr $A + $B`
如,計算user1,user2,user3用戶的UID之和
#!/bin/bash
uid1=`id -u user1`
uid2=`id -u user2`
uid3=`id -u user3`
uid_sum=$[$uid1 + $uid2 + $uid3]
echo "The sum of uid if $uid_sum."
增強型賦值
bash中的增強型賦值類似於C語言,可較高效的實現引用並賦值的操作,以變量A
與B
爲例:
增強賦值 | 等效操作 |
---|---|
A+=B |
A=$A+$B |
A*=B |
A=$A*$B |
A-=B |
A=$A-$B |
A/+B |
A=$A/$B |
A%=B |
A=$A%$B |
5. 腳本的執行與測試
執行腳本
同其他編程不同,所謂的bash編程,調用的是系統上已有的程序命令,通過bash內置的變量、流程控制等機制實現的。
- 程序的執行方式
-
-
- 編譯執行
- 由源代碼直接一次性編譯爲而進行文件,可直接執行
-
-
-
- 解釋執行
- 程序文件內核無法直接理解,需要外部程序解釋源文件,逐條執行
-
代碼的源文件問文本格式,而內核只能執行二進制格式文件,故直接執行該文件內核是無法理解的,而bash是解釋執行的,需要爲其程序文件執行一個解釋器。
在程序文件內容的最開始處使用以 #! 開頭,後跟解釋器程序路徑的字符串來通知內核,使用該程序對文件進行解釋執行,而不是直接執行,這個字符串被稱爲 Shebang 。
#!/PATH/TO/SHELL_INTERPRETER
事實上在Linux系統中,解釋型語言普遍都是採用以上方式,如Python、Java等代碼,此處以bash腳本爲例,可指定爲
#!/bin/bash
上文程序的第一行就是Shebang。
若不指定,則腳本無法直接執行,但可以明確指定解釋器,將該腳本文件當做參數讓解釋器執行,如上面的文件爲:
uid1=`id -u user1`
uid2=`id -u user2`
uid3=`id -u user3`
uid_sum=$[$uid1 + $uid2 + $uid3]
echo "The sum of uid if $uid_sum."
可使用bash SCRIPT_NAME
執行之
另外,默認新建的文件是沒有執行權限的,若要直接執行,則需要爲其賦予執行權限2:
chmod +x /PATH/TO/SCRPTE
測試腳本
測試腳本中是否有語法錯誤:
bash -n SCRIPT
調試腳本(單步執行)
bash -x SCRIPT
如,腳本文件test.sh
內容爲
#!/bin/bash
declare stra=root
[[ $stra ~= oot ]] && echo Yes || echo No
進行語法測試:
[root@localhost ~]# bash -n scripts/test.sh
scripts/test.sh: line 4: conditional binary operator expected
scripts/test.sh: line 4: syntax error near `~='
scripts/test.sh: line 4: `[[ $stra ~= oot ]] && echo Yes || echo No'
將文件中的~=
修改爲=~
再次測試則沒有報錯,表示無語法錯誤。
單步執行1-10中的偶數和,test.sh
文件內容爲:
#!/bin/bash
declare -i sum=0
for((i=1;i<=10;i++));do
if [ $[$i % 2] -eq 0 ];then
let sum+=$i
fi
done
echo $sum
[root@localhost ~]# bash -x scripts/test.sh
+ declare -i sum=0
+ (( i=1 ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=2
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=4
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=6
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=8
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=10
+ (( i++ ))
+ (( i<=10 ))
+ echo 30
30
6. 循環
有時需要重複執行一些特定的操作,比如,對某文件的每一行內容做特定操作,循環執行某計算知道某特定條件滿足。此時可使用循環控制,bash中提供三種基本的循環控制語句。
while
while的基本使用格式爲:
while CONDITION; do
STATEMENTS
done
CONDITION爲進入循環的條件,是布爾表達式,其結果爲真時進入循環。
如:計算100以內整數的和
#!/bin/bash
declare -i I=1
declare -i SUM=0
while [ $I -le 100 ]; do
let SUM+=$I
let I++
done
echo $SUM
until
until的作用於while相似,不同的是當條件爲假時進入循環,其使用格式爲
until CONDITION; do
STATEMENS
done
如使用until計算100以內整數的和
#!/bin/bash
declare -i I=1
declare -i SUM=0
until [ $I -gt 100 ]; do
let SUM+=$I
let I++
done
echo $SUM
for
bash風格的for語句
for的基本使用格式爲:
for VAR in LIST; do
COMMANDS;
done
LIST是提供了一系列用於迭代的值的列表,在for語句執行時,LIST中的每一個元素將在每次循環時賦給變量VAR,在循環體內部可飲用VAR進行相應操作。
列表的生成方式
列表由以下幾種常見的生成方式
- 直接給出列表
- 整數列表
- 能返回列表的命令
- Glob
- 變量引用
直接給出列表
for i in one two three; do
echo $i
done
執行結果:
one
two
three
整數列表
- {START…END},如
for i in {1..10}; do # 將生成1 2 3 4 5 6 7 8 9 10
能返回列表的命令
某些命令的直接結果就是以列表形式給出的,可以使用命令替換使用該結果,如
for i in `ls /`; do
echo $i
done
Tips:使用seq命令生成
seq命令可生成一個數字列表,其使用格式爲
seq [START [STEP]] END
如
[root@localhost scripts]# seq 3
1
2
3
[root@localhost scripts]# seq 3 6
3
4
5
6
[root@localhost scripts]# seq 1 2 10
1
3
5
7
9
Glob
bash的Glob特性所匹配到的各個結果也是以列表的形式存在,如
for file in /etc/*.conf
...
done
代碼將遍歷/etc目錄中以.conf
結尾的文件
變量引用
此處需要指出的是兩個特殊變量$@
與$*
,他們都存儲了傳遞給腳本的參數,其區別只有在雙引號中體現出來。假設在腳本運行時寫了三個參數(分別存儲在$1 $2 $3
)則$*
等價於 “$1 $2 $3
”(傳遞了一個參數);而$@
等價於 “$1
” “$2
” “$3
”(傳遞了三個參數)
C風格的for語句
語法格式
for((INIT_EXPR;EXIT_COND;ITER_EXPR)); do
COMMANDS;
done
這種for語句與C語言的語法類似,INIT_EXPR爲變量的初始化表達式,EXIT_COND爲循環退出的條件測試,結果爲假時退出循環,ITER_EXPR爲每次循環的迭代操作,常用作修正循環變量,如,計算[1,100]的整數和的實現爲:
declare -i sum=0
for((i=1;i<=100;i++)); do
let sum+=$i
done
echo "$sum"
需要說明的是,C風格的寫法中,變量的引用方式、布爾運算符等都都與bash風格有所不同:
- 變量賦值可以包含空格
- 變量引用不必使用$做前綴
- 迭代的處理式不使用expr表達式
循環控制
break
break用於跳出當前循環語句,其後加一個數字可跳出多層循環
continue
continue用於結束本輪循環
如,計算1-100的偶數和:
#!/bin/bash
declare -i sum=0
declare -i idx=1
while true; do
let idx++
if [ $[$idx%2] -ne 0 ]; then
continue
fi
if [ $idx -gt 100 ]; then
break
fi
let sum+=$idx
done
echo $sum
7. 比較與測試
在使用條件判斷時,經常需要通過一系列比較與測試,根據其結果做出相應的操作。此處再次強調,bash中0表示真,非0則爲假
條件測試表達式
可使用一下三種方式
[ EXPRESSION ]
# 中括號與EXPRESSION之間必須有空格,該中括號是命令
[[ EXPRESSION ]]
# 內側的中括號與EXPRESSION之間必須有空格,這兩個中括號是關鍵字
test EXPRESSION
EXPRESSION
爲測試表達式,常用的有以下三類
- 數值測試
- 字符測試
- 文件測試
數值測試
數值比較使用如下形式
NUM1 OPRAND NUM2
OPRAND
爲一個雙目比較的操作操作符,有如下爲常用的操作符
OPRAND | 說明 |
---|---|
-eq |
即equal,測試兩個數是否相等,若是則返回真 |
-ne |
即not equal,測試兩個數是否不等,若是則返回真 |
-gt |
即greater than,測試第一個操作數是否大於第二個操作數,若是返回真 |
-ge |
即greater equal,測試第一個操作數是否大於或等於第二個操作數,若是返回真 |
-lt |
即less than,測試第一個操作數是否小於第二個操作數,若是返回真 |
-le |
即less equal,測試第一個操作數是否小於或等於第二個操作數,若是返回真 |
字符測試
bash中字符測試域數值測試類似,可測試字符或字符串,只是OPRAND
的形式不同
OPRAND | 說明 |
---|---|
== |
測試兩個字符串是否相等,若是則返回真 |
!= |
測試兩個字符串是否不等,若是則返回真 |
> |
測試第一個操作數是否大於第二個操作數,若是返回真 |
>= |
測試第一個操作數是否大於或等於第二個操作數,若是返回真 |
< |
測試第一個操作數是否小於第二個操作數,若是返回真 |
<= |
測試第一個操作數是否小於或等於第二個操作數,若是返回真 |
=~ |
模式匹配,若左側的字符串可以被右側的模式匹配,則返回真 |
字符串大小比較 將逐位比較兩個操作數中每一個字符的大小,若相等則比較下一位,直至比較結果不等,以該結果作爲整個表達式的結果。
關於=~
模式匹配測試,注意一下幾點
- 通常在
[[ ]]
中使用 - 模式中可以使用行首、行尾錨定
- 模式用不用加引號
如,測試stringA中是否包含oot:
#!/bin/bash
declare stringA=root
echo $stringA
[[ $stringA =~ oot ]] && echo "Matched." || echo "Not Match."
stringA=Linux
echo $stringA
[[ $stringA =~ oot ]] && echo "Matched." || echo "Not Match."
輸出結果:
root
Matched.
Linux
Not Match.
此外,字符串測試還有單目測試符:
-n "STRING" 測試指定字符串是否不空,不空爲真
-z "STRING" 測試指定字符串是否爲空,空則爲真
Tips
- 用於字符測試時用到的操作數都應該使用引號
- 不做變量替換使用單引號,做變量替換使用雙引號
- 要使用雙中括號
[[ ]]
文件測試
文件測試使用測試符對文件內容或某屬性進行測試
單目測試
存在性測試
操作符 | 描述 |
---|---|
-e FILE | 測試文件是否存在 |
-a FILE | 測試文件是否存在 |
大小測試(測試文件是否有內容)
操作符 | 描述 |
---|---|
-s FILE | 測試文件是否不空 |
類型測試
操作符 | 描述 |
---|---|
-f FILE | 測試文件是否爲普通文件 |
-d FILE | 測試文件是否爲路徑(即目錄) |
-b FILE | 測試文件是否爲塊設備文件 |
-c FILE | 測試文件是否爲字符設備文件 |
-h FILE | 測試文件是否爲符號鏈接文件,同-L |
-L FILE | 測試文件是否爲符號鏈接文件,同-h |
-p FILE | 測試文件是否爲命名管道文件 |
-S FILE | 測試文件是否爲套接字文件 |
若文件不存在,則測試結果直接爲假
權限測試
操作符 | 描述 |
---|---|
-r FILE | 測試當前用戶對指定文件是否有讀權限 |
-w FILE | 測試當前用戶對指定文件是否有寫權限 |
-x FILE | 測試當前用戶對指定文件是否有執行權限 |
若文件不存在,則測試結果直接爲假
特殊權限測試
操作符 | 描述 |
---|---|
-g FILE | 測試文件是否設置了SGID |
-u FILE | 測試文件是否設置了SUID |
-k FILE | 測試文件是否設置了sticky |
打開性測試
操作符 | 描述 |
---|---|
-t FD | FD表示文件描述符,是否已經打開且與某終端相關 |
時間戳測試
操作符 | 描述 |
---|---|
-N FILE | 文件自上一次被讀取之後是否被修改過 |
從屬關係測試
操作符 | 描述 |
---|---|
-O FILE | 當前有效用戶是否爲文件屬主 |
-G FILE | 當前有效用戶是否爲文件屬組 |
雙目測試
操作符 | 描述 |
---|---|
FILE1-nt FILE2 | 若FILE1比FILE2更新,則爲真(若FILE1存在,FILE2不存在,也爲真) |
FILE1 -ot FILE2 | 若FILE1比FILE2更老,則爲真 |
FILE1 -ef FILE2 | 若FILE1與FILE2引用了相同的設備以及inode,則爲真 |
例
- 測試/etc/inittab文件是否存在
[ -e /etc/inittab ]
- 測試/etc/rc.d/rc.sysinit文件是否可執行
[ -x /etc/rc.d/rc.sysinit ]
- 寫一個腳本,給定一個文件,如果是一個普通文件,就顯示之,如果是一個目錄,亦顯示之,否則,此爲無法識別之文件
#!/bin/bash FILE=/etc/rc.d/rc.sysinit if [ ! -e $FILE ]; then echo "No such file." exit6 fi if [ -f $FILE ]; then echo "Common file." elif [ -d $FILE ];then echo "Directory." else echo "Unknown." fi
- 寫一個腳本,完成:
- 1、分別複製/var/log下的文件至/tmp/logs目錄中
- 2、複製目錄時,使用cp -r
- 3、複製文件是,使用cp
- 4、複製鏈接文件,使用cp -d
- 5、餘下的類型,使用cp -a
#!/bin/bash targetDir='/tmp/logs' [ -e $targetDir ] || mkdir $targetDir for fileName in /var/log/*; do if [ -d $fileName ]; then copyCommand='cp -r' elif [ -f $fileName ]; then copyCommand='cp' elif [ -h $fileName ]; then copyCommand='cp -d' else copyCommand='cp -a' fi $copyCommand $fileName $targetDir done
7. 腳本的參數
我們在執行命令時可以在命令後附加一個或多個參數,在腳本中也支持傳遞參數,向腳本傳遞參數的方式與使用命令相同,即SCRIPT ARG1 ARG2 ...
而傳遞的參數在腳本中可以向變量一樣使用,這些參數存儲在特定變量中。
向腳本傳遞的參數將被bash依次傳遞給$1
,$2
,$3
……,如./test.sh root /etc/fstab linux
中
$1
爲root,$2
爲/etc/fstab,$3
爲linux
此處再次列出相關的特殊變量3
特殊變量 | 描述 |
---|---|
$? |
上一條命令的退出狀態碼 |
$# |
參數的個數 |
$* |
參數列表,會合並各個參數 |
$@ |
參數列表,不會合並參數 |
$0 |
執行的命令或腳本名 |
shift
有時,我們事先並不知道用戶會傳遞多少個參數給腳本,而又需要處理各個參數,如計算給出的所有數字的和。此時可以使用shift命令來切換各參數,即若有多個參數,shift後,當前參數剔除,先一個參數變爲當前參數,以此類推,使用格式爲:
shift [N]
#N爲數字,可省略,默認是1
如,腳本代碼內容爲
#!/bin/bash
echo $1
shift
echo $1
shift
echo $1
若給出了3個參數,則腳本將依次顯示之
腳本返回值
默認情況下,當腳本的最後一條命令執行完成後,腳本將退出,我們也可以使用exit
來顯式控制腳本退出,使用方式爲
exit RETURN_CODE
exit
後指定一個退出狀態碼,即$?
所查看的值,再次說明,0表示執行成功,而執行失敗的值沒有具體規定,但是一般將使用如下習慣:
代碼 | 描述 |
---|---|
0 | 命令成功完成 |
1 | 通常的未知錯誤 |
2 | 誤用shell命令 |
126 | 命令無法執行 |
127 | 沒有找到命令 |
128 | 無效的退出命令 |
128+x | 使用Linux信號x的致命錯誤 |
130 | 使用Ctrl+C終止命令 |
255 | 規範以外的退出狀態 |
例
- 獲取用戶id號(不使用id命令),若其uid與gid相同,則顯示Good guy.,否則顯示Bad guy.
#!/bin/bash USERNAME=$1 if ! grep "^$USERNAME\>" /etc/passwd &> /dev/null; then echo "No such user: $USERNAME." exit 1 fi USERID=`grep "^$USERNAME\>" /etc/passwd | cut -d: -f3` GROUPID=`grep "^$USERNAME\>" /etc/passwd | cut -d: -f4` if [ $USERID -eq $GROUPID ]; then echo "Good guy." else echo "Bad guy." fi
8. 數組
關於數組的基礎用法(聲明,引用,賦值),前文已有說明, 此處從其他角度進行進一步介紹。
引用數組中的所有元素
使用數組名將引用數組的第一個元素,要引用其所有元素,可使用${ARRAY_NAME[*]}
的形式
獲取數組中某一元素的長度
${#ARRAY_NAME[INDEX]}
,同理,${#ARRAY_NAME}
將獲取數組中第一個元素的長度
${#ARRAY_NAME[*]}
與${#ARRAY_NAME[@]}
可獲取數組中所有有效元素的個數
因此,可以使用如下方式向數組中追加元素:
ARRAY_NAME[$#{ARRAY_NAME[*]}]=VAR
數組元素切片
數組切片的使用語法爲
${ARRAY_NAME[@]:OFFSET:NUMBER}
# OFFSET:偏移量
# NUMBER:獲取的元素個數
如
[root@localhost ~]# files=(/etc/[Pp]*)
[root@localhost ~]# echo $files
/etc/pam.d
[root@localhost ~]# echo ${files[*]}
/etc/pam.d /etc/passwd /etc/passwd- /etc/pbm2ppa.conf /etc/php.d /etc/php.ini /etc/pinforc /etc/pkcs11 /etc/pki /etc/plymouth /etc/pm /etc/pnm2ppa.conf /etc/polkit-1 /etc/popt.d /etc/postfix /etc/ppp /etc/prelink.conf.d /etc/printcap /etc/profile /etc/profile.d /etc/protocols /etc/pulse /etc/python
[root@localhost ~]# echo ${files[*]:2:4}
/etc/passwd- /etc/pbm2ppa.conf /etc/php.d /etc/php.ini
9. 字符串處理
字符串切片
字符串切片的使用格式爲${VAR:OFFSET:NUMBER}
,OFFSET
默認爲0,NUMBER
默認爲0,如
[root@localhost ~]# name=Jerry
[root@localhost ~]# echo ${name:2:2}
rr
[root@localhost ~]# echo ${name::1}
J
可以使用負數,實現從右向左截取:
[root@localhost ~]# name=Jerry
[root@localhost ~]# echo ${name: -1}
y
[root@localhost ~]# echo ${name: -4}
erry
注意,冒號後有空格
基於模式取子串
${VAR#*word}
word
爲指定的分隔符- 功能:自左而右搜索,刪除
VAR
字符串開頭至第一次出現word
之間的所有字符,如
[root@localhost scripts]# mypath="/etc/init.d/functions" [root@localhost scripts]# echo ${mypath#*e} tc/init.d/functions
${VAR##*word}
word
爲指定的分隔符- 功能:自左而右搜索,刪除
VAR
字符串開頭至最後一次出現word
之間的所有字符,如
[root@localhost scripts]# mypath="/etc/init.d/functions" [root@localhost scripts]# echo ${mypath##*/} functions
${VAR%word*}
word
爲指定的分隔符- 功能:自右而左搜索,刪除
VAR
字符串末尾至第一次出現word
(包括word
)之間的所有字符,如
[root@localhost scripts]# mypath="/etc/init.d/functions" [root@localhost ~]# echo ${mypath%/*} /etc/init.d
${VAR%%word*}
word
爲指定的分隔符- 功能:自右而左搜索,刪除
VAR
字符串末尾至最後一次出現word
(包括word
)之間的所有字符,如
[root@localhost scripts]# mypath="/etc/init.d/functions" [root@localhost ~]# echo ${mypath%%/*} [root@localhost scripts]# mypath="etc/init.d/functions" [root@localhost ~]# echo ${mypath%%/*} etc
查找替換
-
${var/PATTERN/SUBSTI}
- 查找var所表示的字符串中,將第一次被
PATTERN
所匹配到的字符串,替換爲SUBSTI
所表示的字符串 ${var/PATTERN}
:以PATTERN
爲模式查找var
中第一次匹配到的內容,將其刪除
-
${var//PATTERN/SUBSTI}
- 查找var所表示的字符串中,將所有被
PATTERN
所匹配到的字符串,全部替換爲SUBSTI所表示的字符串 ${var//PATTERN}
:以PATTERN爲模式查找var中所有匹配到的內容,將其刪除
-
${var/#PATTERN/SUBSTI}
- 查找
var
所表示的字符串中,行首被PATTERN
所匹配到的字符串,替換爲SUBSTI所表示的字符串 ${var/#PATTERN}
:以PATTERN
爲模式查找var中行首被匹配到的內容,將其刪除
-
${var/%PATTERN/SUBSTI}
- 查找var所表示的字符串中,行尾被
PATTERN
所匹配到的字符串,替換爲SUBSTI
所表示的字符串 ${car/%PATTERN}
:以PATTERN
爲模式查找var
中行尾被匹配到的內容,將其刪除
字符串大小寫轉換
${var^^}
:將var
中的所有小寫字符轉換爲大寫${var,,}
:將var
中的所有大寫字符轉換爲小寫
如
[root@localhost ~]# str=abcABC
[root@localhost ~]# echo ${str^^}
ABCABC
[root@localhost ~]# echo ${str,,}
abcabc
[root@localhost ~]# echo $str
abcABC
10. 函數
在腳本中若有大量重複且複雜的操作,開發者若也一遍一遍地寫大量重複的代碼,這顯示時及其低效的,此時可以使用函數(Function) 來 處理,其直接作用之一就是代碼重用
定義函數
函數的定義使用關鍵字function
,可使用如下兩種方式使用
function FUNC_NAME {
COMMAND
}
# 此處的小括號應緊跟在函數名之後
FUNC_NAME() {
COMMAND
}
調用函數
函數的調用直接給出函數名即可,若函數支持參數,直接在函數名後給出各參數,使用空格分隔即可。
如,寫一個腳本,完成如下功能
- 1、顯示如下菜單
disk) show disk info
mem) show memory info
cpu) show cpuinfo - 2、顯示用戶選定的內容;
#!/bin/bash
ShowMenu() {
cat << EOF
disk) show disk info
mem) show memory info
cpu) show cpu info
EOF
}
main() {
ShowMenu
read -p "Please choose an option: " option
case $option in
disk)
df -h
;;
mem)
free -m
;;
cpu)
cat /proc/cpuinfo
;;
*)
echo "Wrong option"
esac
}
main
函數的返回值
事實上,函數也可看做是更小型的shell腳本,故其返回值與腳本類似,此處的說明相信很好理解
函數的執行狀態返回值
同腳本相同,函數中最後一條指令的執行狀態返回值即爲函數的執行狀態返回值,不同的是,在腳本中可使用exit
退出腳本並執行執行轉檯碼,而函數中使用return
函數的運行結果
函數的運行結果的引用依然類似於腳本(或命令),函數中的輸出語句(如echo
、printf
等)、函數中的命令執行結果都可作爲函數的運行結果
執行狀態返回值保存在變量$?
中,而執行結果的引用要使用命令引用4
函數中的變量
局部變量的使用
若在函數中使用了在主程序中聲明的變量,重新賦值會修改主程序中的變量。如果不期望函數與主程序中的變量衝突,函數中使用變量都用local
修飾,即使用局部變量
在函數中使用了在主程序中沒有聲明的變量,在函數執行結束後即被撤銷,無論是否使用了local
修飾符
但若在函數中沒有使用declare
,直接聲明,如A=10
,則變量爲全局的
函數的參數
如上所述,可以向給腳本傳遞參數一樣給函數傳參,需要注意的是函數的$1
與腳本的$1
不同,如
調用腳本時使用SCRIPT_FILE ARG1 ARG2 ARG3
,在該腳本中有一行內容FUNC1 root $1 $2
,則,此時,對於腳本而言,$1
、$2
、$3
分別爲ARG1
、ARG2
、ARG3
,而對於函數而言,$1
、$2
、$3
分別爲root
、$ARG1
、$ARG2
可以把腳本的全部位置參數,統統傳遞給腳本中某函數使用:$*
例
- 1、寫一個腳本,判定172.16.0.0網絡內有哪些主機在線,在線的用綠色顯示,不在線的用紅色顯示;要求,編程中使用函數
#!/bin/bash
CnetPing(){
for i in {0..255}; do
ping -c 1 -w 1 $1.$i
done
}
BnetPing(){
for j in {0..255}; do
CnetPing $1.$j
done
}
AnetPing(){
for m in {0.255}; do
BnetPing $1.$m
done
}
netType=`echo $1 | cut -d'.' -f1`
if [[ $netType -gt 0 -a $netType -le 126 ]]; then
AnetPing $1
elif [[ $netType -ge 128 -a $netType -le 191 ]]; then
BnetPing $1
elif [[ $netType -ge 192 -a $netType -le 223 ]]; then
CentPing $1
else
echo "Wrong"
exit 3
fi
- 2、寫一個腳本,完成如下功能(使用函數):
- 1、腳本使用格式:
mkscript.sh [-D|–description “script description”] [-A|–author “script author”] /path/to/somefile - 2、如果文件事先不存在,則創建;且前幾行內容如下所示:
#!/bin/bash
# Description: script description
# Author: script author
# - 3、如果事先存在,但不空,且第一行不是“#!/bin/bash”,則提示錯誤並退出;
如果第一行是“#!/bin/bash”,則使用vim打開腳本;
把光標直接定位至最後一行 - 4、打開腳本後關閉時判斷腳本是否有語法錯誤
如果有,提示輸入y繼續編輯,輸入n放棄並退出;
如果沒有,則給此文件以執行權限;
- 1、腳本使用格式:
#!/bin/bash
showMenu() {
while true; do
if [[ $# -lt 5 ]]; then
echo "mkscript.sh [-D|--description script description] [-A|--author script author] /path/to/somefile"
exit 6
else
return 0
fi
done
}
option() {
case $1 in
-D|--description)
authName=$4
desInfo=$2
;;
-A|--author)
authName=$2
desInfo=$4
;;
*)
showMenu
;;
esac
}
creatFile() {
if [[ -f $1 ]]; then
if [ `head -1 $1` == "#!/bin/bash" ]; then
vim + $1
else
echo "Wrong"
exit 5
fi
else
touch $1 && echo -e "#!/bin/bash\n# Description: $desInfo\n# Author: $authName\n#\n" > $1
vim + $1
fi
}
reedit() {
read -p "Press y to edit,n to exit: " choose
if [ "$choose" == "y" ]; then
vim $1
fi
if [ "$choose" == "n" ]; then
exit 0
fi
}
syntax() {
bash -n $1 &> /dev/null && chmod +x $1 || reedit $1
}
showMenu $*
option $*
creatFile $5
syntax $5
11. 信號捕捉
在腳本中可以直接捕獲信號,以避免內其中的命令捕獲而影響執行5
首先,此處將常用信號再次列出
信號 | 值 | 描述 |
---|---|---|
1 | SIGHUP | 掛起進程,讓一個進程不必重啓即可重讀其配置文件 |
2 | SIGINT | 終端進程,Ctrl+C |
9 | SIGKILL | 殺死進程 |
15 | SIGTERM | 終止一個進程 |
18 | SIGCONT | 調回後臺進程 |
19 | SIGSTOP | 停止一個進程,即送往後臺,Ctrl+Z |
在腳本中可以捕獲信號,但是一般9與15信號不能被捕捉,使用trap
可實現信號捕捉,其使用格式爲
trap 'COMMAND' SIGNALS
捕捉到信號SIGNALS
後,執行COMMAND
同kill -l
,也可以使用trap -l
查看信號列表,一般,腳本中經常需要被捕獲的信號爲SIGHUP與SIGINT
例
#!/bin/bash
#
trap 'echo "quit"; exit 5' INT
for i in {1..254}; do
if ping -w 1 -c 1 172.16.254.$i &> /dev/null; then
echo "172.16.254.$i is up."
else
echo "172.16.254.$i is down."
fi
done
#!/bin/bash
declare -a hosttmpfiles
trap 'mytrap' INT
mytrap() {
echo "Quit"
rm -f ${hosttmpfiles[@]}
exit 1
}
for i in {1..50}; do
tempfile=`mktemp /tmp/ping.XXXX`
if ping -W 1 -c 1 192.168.18.$i &> /dev/null; then
echo "192.168.18.$i is up." | tee $tempfile
else
echo "192.168.18.$i is down." | tee $tempfile
fi
hosttmpfiles[${#hosttmpfiles[*]}]=$tempfile
done
rm -f ${hosttmpfiles[@]}
四、例
- 1、傳遞一個用戶名參數給腳本,判斷此用戶的用戶名跟其基本組的組名是否一致,並將結果顯示出來
#!/bin/bash
if ! id $1 &>/dev/null; then
echo "No such user."
ecit 10
fi
if [ $1 == `id -n -g $1` ]; then
echo "Yiyang"
else
echo "Bu Yiyang"
fi
- 2、傳遞一個參數(單字符就行)給腳本,如參數爲q、Q、quit或Quit,就退出腳本,否則,就顯示用戶的參數
#!/bin/bash
if [ $1 = 'q' ];then
echo "Quiting..."
exit 1
elif [ $1 = 'Q' ];then
echo "Quiting..."
exit 2
elif [ $1 = 'quit' ];then
echo "Quiting..."
exit 3
elif [ $1 = 'Quit' ];then
echo "Quiting..."
exit 4
else
echo $1
fi
- 3、判定所有用戶是否擁有可登陸shell
#!/bin/bash
for userName in `cut -d: -f1 /etc/passwd`; do
if [[ `grep "^$userName\>" /etc/passwd | cut -d: -f7` =~ sh$ ]]; then
echo "login user: $userName."
else
echo "nologin user: $userName."
fi
done
- 4、接受一個參數,若爲–add,添加用戶user1…user10,若爲–del, 刪除用戶user1…user10,其它:退出
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Usage:adminusers ARG"
exit 7
fi
if [ $1 == '--add' ]; then
for I in {1..10}; do
if id user$I &> /dev/null; then
echo "user$I exists."
else
useradd user$I
echo user$I | passwd --stdin user$I &> /dev/null
echo "Add user$I finished."
fi
done
elif [ $1 == '--del' ]; then
for I in {1..10}; do
if id user$I &> /dev/null; then
userdel -r user$I
echo "Delete user$I finished."
else
echo "No user$I."
fi
done
else
echo "Unknown ARG"
exit 8
- 5、在上述基礎上,在–add或–del後加上用戶名作爲參數列表,要求添加或刪除參數列表中給出的用戶,並且給出幫助信息
#!/bin/bash
if [ $1 == '--add' ]; then
for I in `echo $2 | sed 's/,/ /g'`; do
if id $I &> /dev/null; then
echo "$I exists."
else
useradd $I
echo $I | passwd --stdin $I &> /dev/null
echo "Add $I finished."
fi
done
elif [ $1 == '--del' ]; then
for I in `echo $2 | sed 's/,/ /g'`; do
if id $I &> /dev/null; then
userdel -r $I
echo "Delete $I finished."
else
echo "$I NOT exist."
fi
done
elif [ $1 == '--help' ]; then
echo "Usage:adminuser2.sh --add USER1,USER2,... | --del USER1,USER2,... | --help"
else
echo "Unknown options."
fi
- 6、寫一個腳本,使用形式如下:
userinfo.sh -u username [-v {1|2}]
-u選項用於指定用戶,而後腳本顯示用戶的UID和GID
若同時使用了-v:
-v後面的值若是1,則額外顯示用戶udev家目錄路徑
-v後面的值若爲2,則額外顯示用戶的家目錄路徑和shell;
#!/bin/bash
[ $# -lt 2 ] && echo "Too less argements,quit" && exit 3
if [[ "$1" == "-u" ]]; then
userName="$2"
shift 2
fi
if [ $# -ge 2 ] && [ "$1" == "-v" ]; then
verFlag=$2
fi
verFlag=${verFlag:-0}
if [ -n $verFlag ]; then
if ! [[ $verFlag =~ [012] ]]; then
echo "Wrong parameter."
echo "Usage: `basename $0` -u UserName -v {1|2}"
exit 4
fi
fi
if [ $verFlag -eq 1 ]; then
grep "^$userName" /etc/passwd | cut -d: -f1,3,4,6
elif [ $verFlag -eq 2 ]; then
grep "^$userName" /etc/passwd | cut -d: -f1,3,4,6,7
else
grep "^$userName" /etc/passwd | cut -d: -f1,3,4
fi
- 7、寫一個腳本,能對/etc/目錄進行打包備份,備份位置爲/backup/etc-日期.後綴
1、顯示如下菜單給用戶:
xz) xz compress
gzip) gzip compress
bip2) bzip2 compress
2、根據用戶指定的壓縮工具使用tar打包壓縮;
3、默認爲xz;輸入錯誤則需要用戶重新輸入;
#!/bin/bash
[ -d /backup ] || mkdir /backup
cat << EOF
Plz choose a compress tool:
xz) xz compress
gzip) gzip compress
bzip2) bzip2 compress
EOF
while true; do
read -p "Your option: " option
option=${option:-xz}
case $option in
xz)
compressTool='J'
suffix='xz'
break;;
gzip)
compressTool='z'
suffix='gz'
break;;
bzip2)
compressTool='j'
suffix='bz2'
break;;
*)
echo "Wrong option." ;;
esac
done
tar ${compressTool}cf /backup/etc-`date +%F-%H-%M-%S`.tar.$suffix /etc/*
關於bash的特性,詳見這篇文章 https://blog.csdn.net/xiyangyang410/article/details/85090293#bash_385 ↩︎
關於權限的相關介紹 https://blog.csdn.net/xiyangyang410/article/details/85090293#_1324 ↩︎
關於bash變量,詳見 https://blog.csdn.net/xiyangyang410/article/details/85454040#bash_156 ↩︎
命令引用相關內容詳見 https://blog.csdn.net/xiyangyang410/article/details/85090293#_555 ↩︎
關於信號相關內容,後續將詳細介紹 ↩︎