Bash 腳本入門(mén)
腳本(script)就是包含一系列命令的一個(gè)文本文件。Shell 讀取這個(gè)文件,依次執(zhí)行里面的所有命令,就好像這些命令直接輸入到命令行一樣。所有能夠在命令行完成的任務(wù),都能夠用腳本完成。
腳本的好處是可以重復(fù)使用,也可以指定在特定場(chǎng)合自動(dòng)調(diào)用,比如系統(tǒng)啟動(dòng)或關(guān)閉時(shí)自動(dòng)執(zhí)行腳本。
Shebang 行
腳本的第一行通常是指定解釋器,即這個(gè)腳本必須通過(guò)什么解釋器執(zhí)行。這一行以#!字符開(kāi)頭,這個(gè)字符稱(chēng)為 Shebang,所以這一行就叫做 Shebang 行。
#!后面就是腳本解釋器的位置,Bash 腳本的解釋器一般是/bin/sh或/bin/bash。
#!/bin/sh
# 或者
#!/bin/bash
#!與腳本解釋器之間有沒(méi)有空格,都是可以的。
如果 Bash 解釋器不放在目錄/bin,腳本就無(wú)法執(zhí)行了。為了保險(xiǎn),可以寫(xiě)成下面這樣。
#!/usr/bin/env bash
上面命令使用env命令(這個(gè)命令總是在/usr/bin目錄),返回 Bash 可執(zhí)行文件的位置。env命令的詳細(xì)介紹,請(qǐng)看后文。
Shebang 行不是必需的,但是建議加上這行。如果缺少該行,就需要手動(dòng)將腳本傳給解釋器。舉例來(lái)說(shuō),腳本是script.sh,有 Shebang 行的時(shí)候,可以直接調(diào)用執(zhí)行。
$ ./script.sh
上面例子中,script.sh是腳本文件名。腳本通常使用.sh后綴名,不過(guò)這不是必需的。
如果沒(méi)有 Shebang 行,就只能手動(dòng)將腳本傳給解釋器來(lái)執(zhí)行。
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh
執(zhí)行權(quán)限和路徑
前面說(shuō)過(guò),只要指定了 Shebang 行的腳本,可以直接執(zhí)行。這有一個(gè)前提條件,就是腳本需要有執(zhí)行權(quán)限??梢允褂孟旅娴拿?,賦予腳本執(zhí)行權(quán)限。
# 給所有用戶(hù)執(zhí)行權(quán)限
$ chmod +x script.sh
# 給所有用戶(hù)讀權(quán)限和執(zhí)行權(quán)限
$ chmod +rx script.sh
# 或者
$ chmod 755 script.sh
# 只給腳本擁有者讀權(quán)限和執(zhí)行權(quán)限
$ chmod u+rx script.sh
腳本的權(quán)限通常設(shè)為755(擁有者有所有權(quán)限,其他人有讀和執(zhí)行權(quán)限)或者700(只有擁有者可以執(zhí)行)。
除了執(zhí)行權(quán)限,腳本調(diào)用時(shí),一般需要指定腳本的路徑(比如path/script.sh)。如果將腳本放在環(huán)境變量$PATH指定的目錄中,就不需要指定路徑了。因?yàn)?Bash 會(huì)自動(dòng)到這些目錄中,尋找是否存在同名的可執(zhí)行文件。
建議在主目錄新建一個(gè)~/bin子目錄,專(zhuān)門(mén)存放可執(zhí)行腳本,然后把~/bin加入$PATH。
export PATH=$PATH:~/bin
上面命令改變環(huán)境變量$PATH,將~/bin添加到$PATH的末尾??梢詫⑦@一行加到~/.bashrc文件里面,然后重新加載一次.bashrc,這個(gè)配置就可以生效了。
$ source ~/.bashrc
以后不管在什么目錄,直接輸入腳本文件名,腳本就會(huì)執(zhí)行。
$ script.sh
上面命令沒(méi)有指定腳本路徑,因?yàn)?code>script.sh在$PATH指定的目錄中。
env 命令
env命令總是指向/usr/bin/env文件,或者說(shuō),這個(gè)二進(jìn)制文件總是在目錄/usr/bin。
#!/usr/bin/env NAME這個(gè)語(yǔ)法的意思是,讓 Shell 查找$PATH環(huán)境變量里面第一個(gè)匹配的NAME。如果你不知道某個(gè)命令的具體路徑,或者希望兼容其他用戶(hù)的機(jī)器,這樣的寫(xiě)法就很有用。
/usr/bin/env bash的意思就是,返回bash可執(zhí)行文件的位置,前提是bash的路徑是在$PATH里面。其他腳本文件也可以使用這個(gè)命令。比如 Node.js 腳本的 Shebang 行,可以寫(xiě)成下面這樣。
#!/usr/bin/env node
env命令的參數(shù)如下。
-
-i,--ignore-environment:不帶環(huán)境變量啟動(dòng)。 -
-u,--unset=NAME:從環(huán)境變量中刪除一個(gè)變量。 -
--help:顯示幫助。 -
--version:輸出版本信息。
下面是一個(gè)例子,新建一個(gè)不帶任何環(huán)境變量的 Shell。
$ env -i /bin/sh
注釋
Bash 腳本中,#表示注釋?zhuān)梢苑旁谛惺?,也可以放在行尾?/p>
# 本行是注釋
echo 'Hello World!'
echo 'Hello World!' # 井號(hào)后面的部分也是注釋
建議在腳本開(kāi)頭,使用注釋說(shuō)明當(dāng)前腳本的作用,這樣有利于日后的維護(hù)。
腳本參數(shù)
調(diào)用腳本的時(shí)候,腳本文件名后面可以帶有參數(shù)。
$ script.sh word1 word2 word3
上面例子中,script.sh是一個(gè)腳本文件,word1、word2和word3是三個(gè)參數(shù)。
腳本文件內(nèi)部,可以使用特殊變量,引用這些參數(shù)。
-
$0:腳本文件名,即script.sh。 -
$1~$9:對(duì)應(yīng)腳本的第一個(gè)參數(shù)到第九個(gè)參數(shù)。 -
$#:參數(shù)的總數(shù)。 -
$@:全部的參數(shù),參數(shù)之間使用空格分隔。 -
$*:全部的參數(shù),參數(shù)之間使用變量$IFS值的第一個(gè)字符分隔,默認(rèn)為空格,但是可以自定義。
如果腳本的參數(shù)多于9個(gè),那么第10個(gè)參數(shù)可以用${10}的形式引用,以此類(lèi)推。
注意,如果命令是command -o foo bar,那么-o是$1,foo是$2,bar是$3。
下面是一個(gè)腳本內(nèi)部讀取命令行參數(shù)的例子。
#!/bin/bash
# script.sh
echo "全部參數(shù):" $@
echo "命令行參數(shù)數(shù)量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
執(zhí)行結(jié)果如下。
$ ./script.sh a b c
全部參數(shù):a b c
命令行參數(shù)數(shù)量:3
$0 = script.sh
$1 = a
$2 = b
$3 = c
用戶(hù)可以輸入任意數(shù)量的參數(shù),利用for循環(huán),可以讀取每一個(gè)參數(shù)。
#!/bin/bash
for i in "$@"; do
echo $i
done
上面例子中,$@返回一個(gè)全部參數(shù)的列表,然后使用for循環(huán)遍歷。
如果多個(gè)參數(shù)放在雙引號(hào)里面,視為一個(gè)參數(shù)。
$ ./script.sh "a b"
上面例子中,Bash 會(huì)認(rèn)為"a b"是一個(gè)參數(shù),$1會(huì)返回a b。注意,返回時(shí)不包括雙引號(hào)。
shift 命令
shift命令可以改變腳本參數(shù),每次執(zhí)行都會(huì)移除腳本當(dāng)前的第一個(gè)參數(shù)($1),使得后面的參數(shù)向前一位,即$2變成$1、$3變成$2、$4變成$3,以此類(lèi)推。
while循環(huán)結(jié)合shift命令,也可以讀取每一個(gè)參數(shù)。
#!/bin/bash
echo "一共輸入了 $# 個(gè)參數(shù)"
while [ "$1" != "" ]; do
echo "剩下 $# 個(gè)參數(shù)"
echo "參數(shù):$1"
shift
done
上面例子中,shift命令每次移除當(dāng)前第一個(gè)參數(shù),從而通過(guò)while循環(huán)遍歷所有參數(shù)。
shift命令可以接受一個(gè)整數(shù)作為參數(shù),指定所要移除的參數(shù)個(gè)數(shù),默認(rèn)為1。
shift 3
上面的命令移除前三個(gè)參數(shù),原來(lái)的$4變成$1。
getopts 命令
getopts命令用在腳本內(nèi)部,可以解析復(fù)雜的腳本命令行參數(shù),通常與while循環(huán)一起使用,取出腳本所有的帶有前置連詞線(xiàn)(-)的參數(shù)。
getopts optstring name
它帶有兩個(gè)參數(shù)。第一個(gè)參數(shù)optstring是字符串,給出腳本所有的連詞線(xiàn)參數(shù)。比如,某個(gè)腳本可以有三個(gè)配置項(xiàng)參數(shù)-l、-h、-a,其中只有-a可以帶有參數(shù)值,而-l和-h是開(kāi)關(guān)參數(shù),那么getopts的第一個(gè)參數(shù)寫(xiě)成lha:,順序不重要。注意,a后面有一個(gè)冒號(hào),表示該參數(shù)帶有參數(shù)值,getopts規(guī)定帶有參數(shù)值的配置項(xiàng)參數(shù),后面必須帶有一個(gè)冒號(hào)(:)。getopts的第二個(gè)參數(shù)name是一個(gè)變量名,用來(lái)保存當(dāng)前取到的配置項(xiàng)參數(shù),即l、h或a。
下面是一個(gè)例子。
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;
h)
echo "h stands for h"
;;
a)
avalue="$OPTARG"
echo "The value provided is $OPTARG"
;;
?)
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"
上面例子中,while循環(huán)不斷執(zhí)行getopts 'lha:' OPTION命令,每次執(zhí)行就會(huì)讀取一個(gè)連詞線(xiàn)參數(shù)(以及對(duì)應(yīng)的參數(shù)值),然后進(jìn)入循環(huán)體。變量OPTION保存的是,當(dāng)前處理的那一個(gè)連詞線(xiàn)參數(shù)(即l、h或a)。如果用戶(hù)輸入了沒(méi)有指定的參數(shù)(比如-x),那么OPTION等于?。循環(huán)體內(nèi)使用case判斷,處理這四種不同的情況。
如果某個(gè)連詞線(xiàn)參數(shù)帶有參數(shù)值,比如-a foo,那么處理a參數(shù)的時(shí)候,環(huán)境變量$OPTARG保存的就是參數(shù)值。
注意,只要遇到不帶連詞線(xiàn)的參數(shù),getopts就會(huì)執(zhí)行失敗,從而退出while循環(huán)。比如,getopts可以解析command -l foo,但不可以解析command foo -l。另外,多個(gè)連詞線(xiàn)參數(shù)寫(xiě)在一起的形式,比如command -lh,getopts也可以正確處理。
變量$OPTIND在getopts開(kāi)始執(zhí)行前是1,然后每次執(zhí)行就會(huì)加1。等到退出while循環(huán),就意味著連詞線(xiàn)參數(shù)全部處理完畢。這時(shí),$OPTIND - 1就是已經(jīng)處理的連詞線(xiàn)參數(shù)個(gè)數(shù),使用shift命令將這些參數(shù)移除,保證后面的代碼可以用$1、$2等處理命令的主參數(shù)。
配置項(xiàng)參數(shù)終止符 --
-和--開(kāi)頭的參數(shù),會(huì)被 Bash 當(dāng)作配置項(xiàng)解釋。但是,有時(shí)它們不是配置項(xiàng),而是實(shí)體參數(shù)的一部分,比如文件名叫做-f或--file。
$ cat -f
$ cat --file
上面命令的原意是輸出文件-f和--file的內(nèi)容,但是會(huì)被 Bash 當(dāng)作配置項(xiàng)解釋。
這時(shí)就可以使用配置項(xiàng)參數(shù)終止符--,它的作用是告訴 Bash,在它后面的參數(shù)開(kāi)頭的-和--不是配置項(xiàng),只能當(dāng)作實(shí)體參數(shù)解釋。
$ cat -- -f
$ cat -- --file
上面命令可以正確展示文件-f和--file的內(nèi)容,因?yàn)樗鼈兎旁?code>--的后面,開(kāi)頭的-和--就不再當(dāng)作配置項(xiàng)解釋了。
如果要確保某個(gè)變量不會(huì)被當(dāng)作配置項(xiàng)解釋?zhuān)鸵谒懊娣派蠀?shù)終止符--。
$ ls -- $myPath
上面示例中,--強(qiáng)制變量$myPath只能當(dāng)作實(shí)體參數(shù)(即路徑名)解釋。如果變量不是路徑名,就會(huì)報(bào)錯(cuò)。
$ myPath="-l"
$ ls -- $myPath
ls: 無(wú)法訪(fǎng)問(wèn)'-l': 沒(méi)有那個(gè)文件或目錄
上面例子中,變量myPath的值為-l,不是路徑。但是,--強(qiáng)制$myPath只能作為路徑解釋?zhuān)瑢?dǎo)致報(bào)錯(cuò)“不存在該路徑”。
下面是另一個(gè)實(shí)際的例子,如果想在文件里面搜索--hello,這時(shí)也要使用參數(shù)終止符--。
$ grep -- "--hello" example.txt
上面命令在example.txt文件里面,搜索字符串--hello。這個(gè)字符串是--開(kāi)頭,如果不用參數(shù)終止符,grep命令就會(huì)把--hello當(dāng)作配置項(xiàng)參數(shù),從而報(bào)錯(cuò)。
exit 命令
exit命令用于終止當(dāng)前腳本的執(zhí)行,并向 Shell 返回一個(gè)退出值。
$ exit
上面命令中止當(dāng)前腳本,將最后一條命令的退出狀態(tài),作為整個(gè)腳本的退出狀態(tài)。
exit命令后面可以跟參數(shù),該參數(shù)就是退出狀態(tài)。
# 退出值為0(成功)
$ exit 0
# 退出值為1(失?。?$ exit 1
退出時(shí),腳本會(huì)返回一個(gè)退出值。腳本的退出值,0表示正常,1表示發(fā)生錯(cuò)誤,2表示用法不對(duì),126表示不是可執(zhí)行腳本,127表示命令沒(méi)有發(fā)現(xiàn)。如果腳本被信號(hào)N終止,則退出值為128 + N。簡(jiǎn)單來(lái)說(shuō),只要退出值非0,就認(rèn)為執(zhí)行出錯(cuò)。
下面是一個(gè)例子。
if [ $(id -u) != "0" ]; then
echo "根用戶(hù)才能執(zhí)行當(dāng)前腳本"
exit 1
fi
上面的例子中,id -u命令返回用戶(hù)的 ID,一旦用戶(hù)的 ID 不等于0(根用戶(hù)的 ID),腳本就會(huì)退出,并且退出碼為1,表示運(yùn)行失敗。
exit與return命令的差別是,return命令是函數(shù)的退出,并返回一個(gè)值給調(diào)用者,腳本依然執(zhí)行。exit是整個(gè)腳本的退出,如果在函數(shù)之中調(diào)用exit,則退出函數(shù),并終止腳本執(zhí)行。
命令執(zhí)行結(jié)果
命令執(zhí)行結(jié)束后,會(huì)有一個(gè)返回值。0表示執(zhí)行成功,非0(通常是1)表示執(zhí)行失敗。環(huán)境變量$?可以讀取前一個(gè)命令的返回值。
利用這一點(diǎn),可以在腳本中對(duì)命令執(zhí)行結(jié)果進(jìn)行判斷。
cd /path/to/somewhere
if [ "$?" = "0" ]; then
rm *
else
echo "無(wú)法切換目錄!" 1>&2
exit 1
fi
上面例子中,cd /path/to/somewhere這個(gè)命令如果執(zhí)行成功(返回值等于0),就刪除該目錄里面的文件,否則退出腳本,整個(gè)腳本的返回值變?yōu)?code>1,表示執(zhí)行失敗。
由于if可以直接判斷命令的執(zhí)行結(jié)果,執(zhí)行相應(yīng)的操作,上面的腳本可以改寫(xiě)成下面的樣子。
if cd /path/to/somewhere; then
rm *
else
echo "Could not change directory! Aborting." 1>&2
exit 1
fi
更簡(jiǎn)潔的寫(xiě)法是利用兩個(gè)邏輯運(yùn)算符&&(且)和||(或)。
# 第一步執(zhí)行成功,才會(huì)執(zhí)行第二步
cd /path/to/somewhere && rm *
# 第一步執(zhí)行失敗,才會(huì)執(zhí)行第二步
cd /path/to/somewhere || exit 1
source 命令
source命令用于執(zhí)行一個(gè)腳本,通常用于重新加載一個(gè)配置文件。
$ source .bashrc
source命令最大的特點(diǎn)是在當(dāng)前 Shell 執(zhí)行腳本,不像直接執(zhí)行腳本時(shí),會(huì)新建一個(gè)子 Shell。所以,source命令執(zhí)行腳本時(shí),不需要export變量。
#!/bin/bash
# test.sh
echo $foo
上面腳本輸出$foo變量的值。
# 當(dāng)前 Shell 新建一個(gè)變量 foo
$ foo=1
# 打印輸出 1
$ source test.sh
1
# 打印輸出空字符串
$ bash test.sh
上面例子中,當(dāng)前 Shell 的變量foo并沒(méi)有export,所以直接執(zhí)行無(wú)法讀取,但是source執(zhí)行可以讀取。
source命令的另一個(gè)用途,是在腳本內(nèi)部加載外部庫(kù)。
#!/bin/bash
source ./lib.sh
function_from_lib
上面腳本在內(nèi)部使用source命令加載了一個(gè)外部庫(kù),然后就可以在腳本里面,使用這個(gè)外部庫(kù)定義的函數(shù)。
source有一個(gè)簡(jiǎn)寫(xiě)形式,可以使用一個(gè)點(diǎn)(.)來(lái)表示。
$ . .bashrc
別名,alias 命令
alias命令用來(lái)為一個(gè)命令指定別名,這樣更便于記憶。下面是alias的格式。
alias NAME=DEFINITION
上面命令中,NAME是別名的名稱(chēng),DEFINITION是別名對(duì)應(yīng)的原始命令。注意,等號(hào)兩側(cè)不能有空格,否則會(huì)報(bào)錯(cuò)。
一個(gè)常見(jiàn)的例子是為grep命令起一個(gè)search的別名。
alias search=grep
alias也可以用來(lái)為長(zhǎng)命令指定一個(gè)更短的別名。下面是通過(guò)別名定義一個(gè)today的命令。
$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期一, 一月 6, 2020
有時(shí)為了防止誤刪除文件,可以指定rm命令的別名。
$ alias rm='rm -i'
上面命令指定rm命令是rm -i,每次刪除文件之前,都會(huì)讓用戶(hù)確認(rèn)。
alias定義的別名也可以接受參數(shù),參數(shù)會(huì)直接傳入原始命令。
$ alias echo='echo It says: '
$ echo hello world
It says: hello world
上面例子中,別名定義了echo命令的前兩個(gè)參數(shù),等同于修改了echo命令的默認(rèn)行為。
指定別名以后,就可以像使用其他命令一樣使用別名。一般來(lái)說(shuō),都會(huì)把常用的別名寫(xiě)在~/.bashrc的末尾。另外,只能為命令定義別名,為其他部分(比如很長(zhǎng)的路徑)定義別名是無(wú)效的。
直接調(diào)用alias命令,可以顯示所有別名。
$ alias
unalias命令可以解除別名。
$ unalias lt
參考鏈接
- How to use getopts to parse a script options, Egidio Docile