你所不了解的Bash:關(guān)于Bash數(shù)組的介紹
進(jìn)入這個(gè)古怪而神奇的 Bash 數(shù)組的世界。
盡管軟件工程師常常使用命令行來(lái)進(jìn)行各種開(kāi)發(fā),但命令行中的數(shù)組似乎總是一個(gè)模糊的東西(雖然不像正則操作符 =~
那么復(fù)雜隱晦)。除開(kāi)隱晦和有疑問(wèn)的語(yǔ)法,Bash 數(shù)組其實(shí)是非常有用的。
稍等,這是為什么?
寫(xiě) Bash 相關(guān)的東西很難,但如果是寫(xiě)一篇像手冊(cè)那樣注重怪異語(yǔ)法的文章,就會(huì)非常簡(jiǎn)單。不過(guò)請(qǐng)放心,這篇文章的目的就是讓你不用去讀該死的使用手冊(cè)。
真實(shí)(通常是有用的)示例
為了這個(gè)目的,想象一下真實(shí)世界的場(chǎng)景以及 Bash 是怎么幫忙的:你正在公司里面主導(dǎo)一個(gè)新工作,評(píng)估并優(yōu)化內(nèi)部數(shù)據(jù)管線(xiàn)的運(yùn)行時(shí)間。首先,你要做個(gè)參數(shù)掃描分析來(lái)評(píng)估管線(xiàn)使用線(xiàn)程的狀況。簡(jiǎn)單起見(jiàn),我們把這個(gè)管道當(dāng)作一個(gè)編譯好的 C++ 黑盒子,這里面我們能夠調(diào)整的唯一的參數(shù)是用于處理數(shù)據(jù)的線(xiàn)程數(shù)量:./pipeline --threads 4
。
基礎(chǔ)
我們首先要做的事是定義一個(gè)數(shù)組,用來(lái)容納我們想要測(cè)試的 --threads
參數(shù):
allThreads=(1 2 4 8 16 32 64 128)
本例中,所有元素都是數(shù)字,但參數(shù)并不一定是數(shù)字,Bash 中的數(shù)組可以容納數(shù)字和字符串,比如 myArray=(1 2 "three" 4 "five")
就是個(gè)有效的表達(dá)式。就像 Bash 中其它的變量一樣,確保賦值符號(hào)兩邊沒(méi)有空格。否則 Bash 將會(huì)把變量名當(dāng)作程序來(lái)執(zhí)行,把 =
當(dāng)作程序的***個(gè)參數(shù)。
現(xiàn)在我們初始化了數(shù)組,讓我們解析它其中的一些元素。僅僅輸入 echo $allThreads
,你能發(fā)現(xiàn),它只會(huì)輸出***個(gè)元素。
要理解這個(gè)產(chǎn)生的原因,需要回到上一步,回顧我們一般是怎么在 Bash 中輸出變量。考慮以下場(chǎng)景:
type="article"
echo "Found 42 $type"
假如我們得到的變量 $type
是一個(gè)單詞,我們想要添加在句子結(jié)尾一個(gè) s
。我們無(wú)法直接把 s
加到 $type
里面,因?yàn)檫@會(huì)把它變成另一個(gè)變量,$types
。盡管我們可以利用像 echo "Found 42 "$type"s"
這樣的代碼形變,但解決這個(gè)問(wèn)題的***方法是用一個(gè)花括號(hào):echo "Found 42 ${type}s"
,這讓我們能夠告訴 Bash 變量名的起止位置(有趣的是,JavaScript/ES6 在 template literals 中注入變量和表達(dá)式的語(yǔ)法和這里是一樣的)
事實(shí)上,盡管 Bash 變量一般不用花括號(hào),但在數(shù)組中需要用到花括號(hào)。這反而允許我們指定要訪(fǎng)問(wèn)的索引,例如 echo ${allThreads[1]}
返回的是數(shù)組中的第二個(gè)元素。如果不寫(xiě)花括號(hào),比如 echo $allThreads[1]
,會(huì)導(dǎo)致 Bash 把 [1]
當(dāng)作字符串然后輸出。
是的,Bash 數(shù)組的語(yǔ)法很怪,但是至少他們是從 0 開(kāi)始索引的,不像有些語(yǔ)言(說(shuō)的就是你,R
語(yǔ)言)。
遍歷數(shù)組
上面的例子中我們直接用整數(shù)作為數(shù)組的索引,我們現(xiàn)在考慮兩種其他情況:***,如果想要數(shù)組中的第 $i
個(gè)元素,這里 $i
是一個(gè)代表索引的變量,我們可以這樣 echo ${allThreads[$i]}
解析這個(gè)元素。第二,要輸出一個(gè)數(shù)組的所有元素,我們把數(shù)字索引換成 @
符號(hào)(你可以把 @
當(dāng)作表示 all
的符號(hào)):echo ${allThreads[@]}
。
遍歷數(shù)組元素
記住上面講過(guò)的,我們遍歷 $allThreads
數(shù)組,把每個(gè)值當(dāng)作 --threads
參數(shù)啟動(dòng)管線(xiàn):
for t in ${allThreads[@]}; do
./pipeline --threads $t
done
遍歷數(shù)組索引
接下來(lái),考慮一個(gè)稍稍不同的方法。不遍歷所有的數(shù)組元素,我們可以遍歷所有的索引:
for i in ${!allThreads[@]}; do
./pipeline --threads ${allThreads[$i]}
done
一步一步看:如之前所見(jiàn),${allThreads[@]}
表示數(shù)組中的所有元素。前面加了個(gè)感嘆號(hào),變成 ${!allThreads[@]}
,這會(huì)返回?cái)?shù)組索引列表(這里是 0 到 7)。換句話(huà)說(shuō)。for
循環(huán)就遍歷所有的索引 $i
并從 $allThreads
中讀取第 $i
個(gè)元素,當(dāng)作 --threads
選項(xiàng)的參數(shù)。
這看上去很辣眼睛,你可能奇怪為什么我要一開(kāi)始就講這個(gè)。這是因?yàn)橛袝r(shí)候在循環(huán)中需要同時(shí)獲得索引和對(duì)應(yīng)的值,例如,如果你想要忽視數(shù)組中的***個(gè)元素,使用索引可以避免額外創(chuàng)建在循環(huán)中累加的變量。
填充數(shù)組
到目前為止,我們已經(jīng)能夠用給定的 --threads
選項(xiàng)啟動(dòng)管線(xiàn)了。現(xiàn)在假設(shè)按秒計(jì)時(shí)的運(yùn)行時(shí)間輸出到管線(xiàn)。我們想要捕捉每個(gè)迭代的輸出,然后把它保存在另一個(gè)數(shù)組中,因此我們最終可以隨心所欲的操作它。
一些有用的語(yǔ)法
在深入代碼前,我們要多介紹一些語(yǔ)法。首先,我們要能解析 Bash 命令的輸出。用這個(gè)語(yǔ)法可以做到:output=$( ./my_script.sh )
,這會(huì)把命令的輸出存儲(chǔ)到變量 $output
中。
我們需要的第二個(gè)語(yǔ)法是如何把我們剛剛解析的值添加到數(shù)組中。完成這個(gè)任務(wù)的語(yǔ)法看起來(lái)很熟悉:
myArray+=( "newElement1" "newElement2" )
參數(shù)掃描
萬(wàn)事具備,執(zhí)行參數(shù)掃描的腳步如下:
allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
runtime=$(./pipeline --threads $t)
allRuntimes+=( $runtime )
done
就是這個(gè)了!
還有什么能做的?
這篇文章中,我們講過(guò)使用數(shù)組進(jìn)行參數(shù)掃描的場(chǎng)景。我敢保證有很多理由要使用 Bash 數(shù)組,這里就有兩個(gè)例子:
日志警告
本場(chǎng)景中,把應(yīng)用分成幾個(gè)模塊,每一個(gè)都有它自己的日志文件。我們可以編寫(xiě)一個(gè) cron 任務(wù)腳本,當(dāng)某個(gè)模塊中出現(xiàn)問(wèn)題標(biāo)志時(shí)向特定的人發(fā)送郵件:
# 日志列表,發(fā)生問(wèn)題時(shí)應(yīng)該通知的人
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")
# 在每個(gè)日志中查找問(wèn)題標(biāo)志
for i in ${!logPaths[@]};
do
log=${logPaths[$i]}
stakeholder=${logEmails[$i]}
numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )
# 如果近期發(fā)現(xiàn)超過(guò) 5 個(gè)錯(cuò)誤,就警告負(fù)責(zé)人
if [[ "$numErrors" -gt 5 ]];
then
emailRecipient="$stakeholder"
emailSubject="WARNING: ${log} showing unusual levels of errors"
emailBody="${numErrors} errors found in log ${log}"
echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
fi
done
API 查詢(xún)
如果你想要生成一些分析數(shù)據(jù),分析你的 Medium 帖子中用戶(hù)評(píng)論最多的。由于我們無(wú)法直接訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),SQL 不在我們考慮范圍,但我們可以用 API!
為了避免陷入關(guān)于 API 授權(quán)和令牌的冗長(zhǎng)討論,我們將會(huì)使用 JSONPlaceholder,這是一個(gè)面向公眾的測(cè)試服務(wù) API。一旦我們查詢(xún)每個(gè)帖子,解析出每個(gè)評(píng)論者的郵箱,我們就可以把這些郵箱添加到我們的結(jié)果數(shù)組里:
endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()
# 查詢(xún)前 10 個(gè)帖子
for postId in {1..10};
do
# 執(zhí)行 API 調(diào)用,獲取該帖子評(píng)論者的郵箱
response=$(curl "${endpoint}?postId=${postId}")
# 使用 jq 把 JSON 響應(yīng)解析成數(shù)組
allEmails+=( $( jq '.[].email' <<< "$response" ) )
done
注意這里我是用 jq 工具 從命令行里解析 JSON 數(shù)據(jù)。關(guān)于 jq
的語(yǔ)法超出了本文的范圍,但我強(qiáng)烈建議你了解它。
你可能已經(jīng)想到,使用 Bash 數(shù)組在數(shù)不勝數(shù)的場(chǎng)景中很有幫助,我希望這篇文章中的示例可以給你思維的啟發(fā)。如果你從自己的工作中找到其它的例子想要分享出來(lái),請(qǐng)?jiān)谔酉路皆u(píng)論。
請(qǐng)等等,還有很多東西!
由于我們?cè)诒疚闹v了很多數(shù)組語(yǔ)法,這里是關(guān)于我們講到內(nèi)容的總結(jié),包含一些還沒(méi)講到的高級(jí)技巧:
語(yǔ)法
效果
arr=()
創(chuàng)建一個(gè)空數(shù)組
arr=(1 2 3)
初始化數(shù)組
${arr[2]}
取得第三個(gè)元素
${arr[@]}
取得所有元素
${!arr[@]}
取得數(shù)組索引
${#arr[@]}
計(jì)算數(shù)組長(zhǎng)度
arr[0]=3
覆蓋第 1 個(gè)元素
arr+=(4)
添加值
str=$(ls)
把
ls
輸出保存到字符串
arr=( $(ls) )
把
ls
輸出的文件保存到數(shù)組里
${arr[@]:s:n}
取得從索引
s
開(kāi)始的 n
個(gè)元素
***一點(diǎn)思考
正如我們所見(jiàn),Bash 數(shù)組的語(yǔ)法很奇怪,但我希望這篇文章讓你相信它們很有用。只要你理解了這些語(yǔ)法,你會(huì)發(fā)現(xiàn)以后會(huì)經(jīng)常使用 Bash 數(shù)組。
Bash 還是 Python?
問(wèn)題來(lái)了:什么時(shí)候該用 Bash 數(shù)組而不是其他的腳本語(yǔ)法,比如 Python?
對(duì)我而言,完全取決于需求——如果你可以只需要調(diào)用命令行工具就能立馬解決問(wèn)題,你也可以用 Bash。但有些時(shí)候,當(dāng)你的腳本屬于一個(gè)更大的 Python 項(xiàng)目時(shí),你也可以用 Python。
比如,我們可以用 Python 來(lái)實(shí)現(xiàn)參數(shù)掃描,但我們只用編寫(xiě)一個(gè) Bash 的包裝:
import subprocess
all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []
# 用不同的線(xiàn)程數(shù)字啟動(dòng)管線(xiàn)
for t in all_threads:
cmd = './pipeline --threads {}'.format(t)
# 使用子線(xiàn)程模塊獲得返回的輸出
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
output = p.communicate()[0]
all_runtimes.append(output)
由于本例中沒(méi)法避免使用命令行,所以可以?xún)?yōu)先使用 Bash。