Bash編程

在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. 編寫代碼的約定

幾乎所有的編程語言,在代碼編寫過程中都會有一些約定俗成的規則,並且也有很多團隊有自己內部的開發規範,這裏對基本的規則予以介紹,以避免養成一些陋習。

  1. 變量名要做到見名知意
    不要出現類似a,b,c,或data1,data2之類的變量名
    拼音並不能很好的做到見名知意,應使用英語
    可使用駝峯法,如PeopleCount,或使用下劃線分隔單詞,如people_count
  2. 代碼需要有註釋
    對於某些複雜邏輯,或一些標識值的意義等,應該在註釋部分做以說明
    在代碼首部,應該有對該代碼文件的相關說明,如代碼的功能介紹、開發日期、作者,有的甚至需要註明版本變更與內容

三、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."
    

德摩根定律

該定律應用很廣泛,此處也將其列出
ABAB \overline{A \bigcap B} \equiv \overline{A} \bigcup \overline{B}

ABAB \overline{A \bigcup B} \equiv \overline{A} \bigcap \overline{B}

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語言,可較高效的實現引用並賦值的操作,以變量AB爲例:

增強賦值 等效操作
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

函數的運行結果

函數的運行結果的引用依然類似於腳本(或命令),函數中的輸出語句(如echoprintf等)、函數中的命令執行結果都可作爲函數的運行結果

執行狀態返回值保存在變量$?中,而執行結果的引用要使用命令引用4

函數中的變量

局部變量的使用

若在函數中使用了在主程序中聲明的變量,重新賦值會修改主程序中的變量。如果不期望函數與主程序中的變量衝突,函數中使用變量都用local修飾,即使用局部變量

在函數中使用了在主程序中沒有聲明的變量,在函數執行結束後即被撤銷,無論是否使用了local修飾符
但若在函數中沒有使用declare,直接聲明,如A=10,則變量爲全局的

函數的參數

如上所述,可以向給腳本傳遞參數一樣給函數傳參,需要注意的是函數的$1與腳本的$1不同,如
調用腳本時使用SCRIPT_FILE ARG1 ARG2 ARG3,在該腳本中有一行內容FUNC1 root $1 $2,則,此時,對於腳本而言,$1$2$3分別爲ARG1ARG2ARG3,而對於函數而言,$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放棄並退出;
      如果沒有,則給此文件以執行權限;
#!/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/*

  1. 關於bash的特性,詳見這篇文章 https://blog.csdn.net/xiyangyang410/article/details/85090293#bash_385 ↩︎

  2. 關於權限的相關介紹 https://blog.csdn.net/xiyangyang410/article/details/85090293#_1324 ↩︎

  3. 關於bash變量,詳見 https://blog.csdn.net/xiyangyang410/article/details/85454040#bash_156 ↩︎

  4. 命令引用相關內容詳見 https://blog.csdn.net/xiyangyang410/article/details/85090293#_555 ↩︎

  5. 關於信號相關內容,後續將詳細介紹 ↩︎

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