你所不了解的 Bash:關於 Bash 數組的介紹
儘管軟體工程師常常使用命令行來進行各種開發,但命令行中的數組似乎總是一個模糊的東西(雖然不像正則操作符 =~
那麼複雜隱晦)。除開隱晦和有疑問的語法,Bash 數組其實是非常有用的。
稍等,這是為什麼?
寫 Bash 相關的東西很難,但如果是寫一篇像手冊那樣注重怪異語法的文章,就會非常簡單。不過請放心,這篇文章的目的就是讓你不用去讀該死的使用手冊。
真實(通常是有用的)示例
為了這個目的,想像一下真實世界的場景以及 Bash 是怎麼幫忙的:你正在公司裡面主導一個新工作,評估並優化內部數據管線的運行時間。首先,你要做個參數掃描分析來評估管線使用線程的狀況。簡單起見,我們把這個管道當作一個編譯好的 C++ 黑盒子,這裡面我們能夠調整的唯一的參數是用於處理數據的線程數量:./pipeline --threads 4
。
基礎
我們首先要做的事是定義一個數組,用來容納我們想要測試的 --threads
參數:
allThreads=(1 2 4 8 16 32 64 128)
本例中,所有元素都是數字,但參數並不一定是數字,Bash 中的數組可以容納數字和字元串,比如 myArray=(1 2 "three" 4 "five")
就是個有效的表達式。就像 Bash 中其它的變數一樣,確保賦值符號兩邊沒有空格。否則 Bash 將會把變數名當作程序來執行,把 =
當作程序的第一個參數。
現在我們初始化了數組,讓我們解析它其中的一些元素。僅僅輸入 echo $allThreads
,你能發現,它只會輸出第一個元素。
要理解這個產生的原因,需要回到上一步,回顧我們一般是怎麼在 Bash 中輸出變數。考慮以下場景:
type="article"
echo "Found 42 $type"
假如我們得到的變數 $type
是一個單詞,我們想要添加在句子結尾一個 s
。我們無法直接把 s
加到 $type
裡面,因為這會把它變成另一個變數,$types
。儘管我們可以利用像 echo "Found 42 "$type"s"
這樣的代碼形變,但解決這個問題的最好方法是用一個花括弧:echo "Found 42 ${type}s"
,這讓我們能夠告訴 Bash 變數名的起止位置(有趣的是,JavaScript/ES6 在 template literals 中注入變數和表達式的語法和這裡是一樣的)
事實上,儘管 Bash 變數一般不用花括弧,但在數組中需要用到花括弧。這反而允許我們指定要訪問的索引,例如 echo ${allThreads[1]}
返回的是數組中的第二個元素。如果不寫花括弧,比如 echo $allThreads[1]
,會導致 Bash 把 [1]
當作字元串然後輸出。
是的,Bash 數組的語法很怪,但是至少他們是從 0 開始索引的,不像有些語言(說的就是你,R
語言)。
遍曆數組
上面的例子中我們直接用整數作為數組的索引,我們現在考慮兩種其他情況:第一,如果想要數組中的第 $i
個元素,這裡 $i
是一個代表索引的變數,我們可以這樣 echo ${allThreads[$i]}
解析這個元素。第二,要輸出一個數組的所有元素,我們把數字索引換成 @
符號(你可以把 @
當作表示 all
的符號):echo ${allThreads[@]}
。
遍曆數組元素
記住上面講過的,我們遍歷 $allThreads
數組,把每個值當作 --threads
參數啟動管線:
for t in ${allThreads[@]}; do
./pipeline --threads $t
done
遍曆數組索引
接下來,考慮一個稍稍不同的方法。不遍歷所有的數組元素,我們可以遍歷所有的索引:
for i in ${!allThreads[@]}; do
./pipeline --threads ${allThreads[$i]}
done
一步一步看:如之前所見,${allThreads[@]}
表示數組中的所有元素。前面加了個感嘆號,變成 ${!allThreads[@]}
,這會返回數組索引列表(這裡是 0 到 7)。換句話說。for
循環就遍歷所有的索引 $i
並從 $allThreads
中讀取第 $i
個元素,當作 --threads
選項的參數。
這看上去很辣眼睛,你可能奇怪為什麼我要一開始就講這個。這是因為有時候在循環中需要同時獲得索引和對應的值,例如,如果你想要忽視數組中的第一個元素,使用索引可以避免額外創建在循環中累加的變數。
填充數組
到目前為止,我們已經能夠用給定的 --threads
選項啟動管線了。現在假設按秒計時的運行時間輸出到管線。我們想要捕捉每個迭代的輸出,然後把它保存在另一個數組中,因此我們最終可以隨心所欲的操作它。
一些有用的語法
在深入代碼前,我們要多介紹一些語法。首先,我們要能解析 Bash 命令的輸出。用這個語法可以做到:output=$( ./my_script.sh )
,這會把命令的輸出存儲到變數 $output
中。
我們需要的第二個語法是如何把我們剛剛解析的值添加到數組中。完成這個任務的語法看起來很熟悉:
myArray+=( "newElement1" "newElement2" )
參數掃描
萬事具備,執行參數掃描的腳步如下:
allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
runtime=$(./pipeline --threads $t)
allRuntimes+=( $runtime )
done
就是這個了!
還有什麼能做的?
這篇文章中,我們講過使用數組進行參數掃描的場景。我敢保證有很多理由要使用 Bash 數組,這裡就有兩個例子:
日誌警告
本場景中,把應用分成幾個模塊,每一個都有它自己的日誌文件。我們可以編寫一個 cron 任務腳本,當某個模塊中出現問題標誌時向特定的人發送郵件:
# 日誌列表,發生問題時應該通知的人
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")
# 在每個日誌中查找問題標誌
for i in ${!logPaths[@]};
do
log=${logPaths[$i]}
stakeholder=${logEmails[$i]}
numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )
# 如果近期發現超過 5 個錯誤,就警告負責人
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 查詢
如果你想要生成一些分析數據,分析你的 Medium 帖子中用戶評論最多的。由於我們無法直接訪問資料庫,SQL 不在我們考慮範圍,但我們可以用 API!
為了避免陷入關於 API 授權和令牌的冗長討論,我們將會使用 JSONPlaceholder,這是一個面向公眾的測試服務 API。一旦我們查詢每個帖子,解析出每個評論者的郵箱,我們就可以把這些郵箱添加到我們的結果數組裡:
endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()
# 查詢前 10 個帖子
for postId in {1..10};
do
# 執行 API 調用,獲取該帖子評論者的郵箱
response=$(curl "${endpoint}?postId=${postId}")
# 使用 jq 把 JSON 響應解析成數組
allEmails+=( $( jq '.[].email' <<< "$response" ) )
done
注意這裡我是用 jq 工具 從命令行里解析 JSON 數據。關於 jq
的語法超出了本文的範圍,但我強烈建議你了解它。
你可能已經想到,使用 Bash 數組在數不勝數的場景中很有幫助,我希望這篇文章中的示例可以給你思維的啟發。如果你從自己的工作中找到其它的例子想要分享出來,請在帖子下方評論。
請等等,還有很多東西!
由於我們在本文講了很多數組語法,這裡是關於我們講到內容的總結,包含一些還沒講到的高級技巧:
語法 | 效果 |
---|---|
arr=() |
創建一個空數組 |
arr=(1 2 3) |
初始化數組 |
${arr[2]} |
取得第三個元素 |
${arr[@]} |
取得所有元素 |
${!arr[@]} |
取得數組索引 |
${#arr[@]} |
計算數組長度 |
arr[0]=3 |
覆蓋第 1 個元素 |
arr+=(4) |
添加值 |
str=$(ls) |
把 ls 輸出保存到字元串 |
arr=( $(ls) ) |
把 ls 輸出的文件保存到數組裡 |
${arr[@]:s:n} |
取得從索引 s 開始的 n 個元素 |
最後一點思考
正如我們所見,Bash 數組的語法很奇怪,但我希望這篇文章讓你相信它們很有用。只要你理解了這些語法,你會發現以後會經常使用 Bash 數組。
Bash 還是 Python?
問題來了:什麼時候該用 Bash 數組而不是其他的腳本語法,比如 Python?
對我而言,完全取決於需求——如果你可以只需要調用命令行工具就能立馬解決問題,你也可以用 Bash。但有些時候,當你的腳本屬於一個更大的 Python 項目時,你也可以用 Python。
比如,我們可以用 Python 來實現參數掃描,但我們只用編寫一個 Bash 的包裝:
import subprocess
all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []
# 用不同的線程數字啟動管線
for t in all_threads:
cmd = './pipeline --threads {}'.format(t)
# 使用子線程模塊獲得返回的輸出
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
output = p.communicate()[0]
all_runtimes.append(output)
由於本例中沒法避免使用命令行,所以可以優先使用 Bash。
羞恥的宣傳時間
如果你喜歡這篇文章,這裡還有很多類似的文章! 在此註冊,加入 OSCON,2018 年 7 月 17 號我會在這做一個主題為 你所不了解的 Bash 的在線編碼研討會。沒有幻燈片,不需要門票,只有你和我在命令行裡面敲代碼,探索 Bash 中的奇妙世界。
本文章由 [Medium] 首發,再發布時已獲得授權。
via: https://opensource.com/article/18/5/you-dont-know-bash-intro-bash-arrays
作者:Robert Aboukhalil 選題:lujun9972 譯者:BriFuture 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive