探索 Shell 腳本的威力
當(dāng)我們登錄到一個 UNIX/Linux 系統(tǒng)時,我們首先注意到的是閃爍的光標和 $
符號之間的空格。這就是
Shell(交互界面)。多年來,它一直是一種無處不在(有時甚至是唯一的)與計算機交互的界面。在圖形用戶界面(GUI)出現(xiàn)和流行之前,終端和
Shell 是唯一的機制,可以讓計算機按照我們的意圖進行操作。乍一看,我們可能會想知道 Shell
的作用,除了將命令傳遞給底層操作系統(tǒng)以進行執(zhí)行之外。我們中的大多數(shù)人熟悉像 ls
(用于列出目錄內(nèi)容),cd
(用于更改當(dāng)前目錄)等命令。通過 Shell,我們可以執(zhí)行這些命令。Shell 理解我們輸入的文本 - 將其轉(zhuǎn)換為標記 - 然后在操作系統(tǒng)上執(zhí)行這些標記。
不同的 Shell 變種
最初,終端使用了樸素的 Bourne Shell(即 Sh)。多年來,許多不同的 Shell 變種被開發(fā)出來和使用。其中一些流行的包括 C Shell(Csh) 和 Korn Shell(Ksh)。Sh 在一段時間內(nèi)不再受歡迎,但通過其最新的化身 —— Bourne Again Shell(Bash),它再次流行起來。
Shell 實際上是做什么的?
Shell 是操作系統(tǒng)(OS)和用戶之間的直接接口。通過使用命令和應(yīng)用程序來使用計算機上安裝的工具,我們可以使計算機按照我們的意愿工作。一些命令是安裝在操作系統(tǒng)上的應(yīng)用程序,而某些命令則是直接內(nèi)置在 Shell 中的。在 Bash 中內(nèi)置的一些命令包括 clear
、cd
、eval
和 exec
,還有 ls
和 mkdir
這樣的應(yīng)用程序。內(nèi)置在 Shell 中的命令因 Shell 而異。
在本文中,我們將涵蓋與 Bash 相關(guān)的幾個方面。
更多關(guān)于 Shell 的內(nèi)容
我們中的大多數(shù)人都用過像 ls
、cd
和 mkdir
這樣的命令。當(dāng)我們在一個目錄上運行 ls -l
命令時,該目錄中的所有子目錄和文件都會在屏幕上列出。如果數(shù)量很大,屏幕會滾動。如果終端不支持滾動條(在很多年里都是如此),則無法查看已滾動過的條目。為了克服這個問題,我們使用像 more
和 less
這樣的命令。它們允許我們逐頁查看輸出。通常使用的命令是:
ls -l | less
在這里 Shell 是在做什么?看起來像是單個命令,實際上是 ls
和 less
兩個命令依次執(zhí)行。管道符(|
)將這兩個程序連接起來,但連接由 Shell 管理。由于有了管道符,Shell 連接了這兩個程序——它將 ls
命令的標準輸出連接到 less
的標準輸入(stdin)。管道功能使我們能夠?qū)⑷魏纬绦虻妮敵鲎鳛榱硪粋€程序的輸入提供,而無需對程序進行任何更改。這是許多 UNIX/Linux 應(yīng)用程序的理念——保持應(yīng)用程序簡單,然后將許多應(yīng)用程序組合在一起以實現(xiàn)最終結(jié)果,而不是讓一個程序做很多事情。
如果需要,我們可以將 ls
的輸出重定向到文件中,然后使用 vi
查看它。為此,我們使用以下命令:
ls -l > /tmp/my_file.txt
vi /tmp/my_file.txt
在這種情況下,ls
的輸出被重定向到一個文件中。這由 Shell 進行管理,它理解 >
符號表示重定向。它將其后面的標記視為文件。
使用 Shell 自動化
結(jié)合命令的能力是使用 Shell 命令創(chuàng)建自動化腳本的關(guān)鍵要素之一。在我最近的項目中,我們使用集群模式執(zhí)行
Python/Spark(PySpark)應(yīng)用程序。每個應(yīng)用程序執(zhí)行了許多結(jié)構(gòu)化查詢語言(SQL)語句 -
SparkSQL。為了跟蹤應(yīng)用程序的進展,我們會打印有關(guān)正在執(zhí)行的 SQL
的詳細信息。這樣可以讓我們保留應(yīng)用程序中正在發(fā)生的情況的日志。由于應(yīng)用程序在集群模式下執(zhí)行,要查看日志,我們必須使用以下 yarn
命令:
yarn log –applicationId [application_id]
在大多數(shù)情況下,應(yīng)用程序生成的日志非常大。因此,我們通常將日志導(dǎo)入到 less
中,或?qū)⑵渲囟ㄏ虻揭粋€文件中。我們使用的命令是:
yarn log –aplicationId [application_id] | less
我們的開發(fā)團隊有 40 人。每個人都必須記住這個命令。為了簡化操作,我將這個命令轉(zhuǎn)換成了一個 Bash 腳本。為此,我創(chuàng)建了一個以 .sh
為擴展名的文件。在 UNIX 和 Linux 系統(tǒng)上,文件擴展名并不重要。只要文件是可執(zhí)行的,它就能工作。擴展名在 MS Windows 上有意義。
需要記住的重要事項
Shell 是一個解釋器。這意味著它會逐行讀取程序并執(zhí)行它。這種方法的限制在于錯誤(如果有)在事先無法被識別。直到解釋器讀取和執(zhí)行它們時,錯誤才會被識別出來。簡而言之,假如我們有一個在前 20 行完美執(zhí)行,但在第 21 行由于語法錯誤而失敗的 Shell 程序。當(dāng)腳本在第 21 行失敗時,Shell 不會回滾/撤銷之前的步驟。當(dāng)發(fā)生這樣的情況時,我們必須糾正腳本并從第一行開始執(zhí)行。因此,例如,如果在遇到錯誤之前刪除了幾個文件,腳本的執(zhí)行將停止,而文件將永遠消失。
我創(chuàng)建的腳本是:
#!/bin/bash
yarn log –applicationId 123 | less
…其中 123 是應(yīng)用程序的 ID。
第一行的前兩個字符是特殊字符(“釋伴”)。它們告訴腳本這是一個可執(zhí)行文件,并且該行包含要用于執(zhí)行的程序的名稱。腳本的其余行傳遞給所提到的程序。在這個例子中,我們將執(zhí)行 Bash。即使包含了第一行,我們?nèi)匀槐仨毷褂靡韵旅顚ξ募?yīng)用執(zhí)行權(quán)限:
chmod +x my_file.sh
在給文件設(shè)置了執(zhí)行權(quán)限之后,我們可以如下執(zhí)行它:
./my_file.sh
如果我們沒有給文件設(shè)置執(zhí)行權(quán)限,我們可以使用以下命令執(zhí)行該腳本:
sh ./my_file.sh
傳遞參數(shù)
你很快就會意識到,這樣的腳本很方便,但立即變得無用。每次執(zhí)行 Python/Spark 應(yīng)用程序時,都會生成一個新的 ID。因此,對于每次運行,我們都必須編輯文件并添加新的應(yīng)用程序 ID。這無疑降低了腳本的可用性。為了提高其可用性,我們應(yīng)該將應(yīng)用程序 ID 作為參數(shù)傳遞:
#!/bin/bash
yarn –log -applicationId ${1} | less
我們需要這樣執(zhí)行腳本:
./show_log.sh 123
腳本將執(zhí)行 yarn
命令,獲取應(yīng)用程序的日志并允許我們查看它。
如果我們想將輸出重定向到一個文件中怎么辦?沒問題。我們可以將輸出重定向到一個文件而不是發(fā)送給 less
:
#!/bin/bash
ls –l ${1} > ${2}
view ${2}
要運行腳本,我們需要提供兩個參數(shù),命令變?yōu)椋?/p>
./my_file.sh /tmp /tmp/listing.txt
當(dāng)執(zhí)行時,$1
將綁定到 /tmp
,$2
將綁定到 /tmp/listing.txt
。對于 Shell,參數(shù)從一到九命名。這并不意味著我們不能將超過九個參數(shù)傳遞給腳本。我們可以,但這是另一篇文章的主題。你會注意到,我將參數(shù)命名為 ${1}
和 ${2}
,而不是 $1
和 $2
。將參數(shù)名稱封閉在花括號中是一個好習(xí)慣,因為它使我們能夠無歧義地將參數(shù)作為較長變量的一部分組合起來。例如,我們可以要求用戶將文件名作為參數(shù),并使用它來形成一個更大的文件名。例如,我們可以將 $1
作為參數(shù),創(chuàng)建一個新的文件名為 ${1}_student_names.txt
。
使腳本更健壯
如果用戶忘記提供參數(shù)怎么辦?Shell 允許我們檢查這種情況。我們將腳本修改如下:
#!/bin/bash
if [ -z "${2}" ]; then
echo "file name not provided"
exit 1
fi
if [ -z "${1}" ]; then
echo "directory name not provided"
exit 1
fi
DIR_NAME=${1}
FILE_NAME=${2}
ls -l ${DIR_NAME} > /tmp/${FILE_NAME}
view /tmp/${FILE_NAME}
在這個程序中,我們檢查是否傳遞了正確的參數(shù)。如果未傳遞參數(shù),我們將退出腳本。你會注意到,我以相反的順序檢查參數(shù)。如果我們在檢查第一個參數(shù)存在之前檢查第二個參數(shù)的存在,如果只傳遞了一個參數(shù),腳本將進行到下一步。雖然可以按遞增順序檢查參數(shù)的存在,但我最近意識到,從九到一檢查會更好,因為我們可以提供適當(dāng)?shù)腻e誤消息。你還會注意到,參數(shù)已分配給變量。參數(shù)一到九是位置參數(shù)。將位置參數(shù)分配給具名參數(shù)可以在出現(xiàn)問題時更容易調(diào)試腳本。
自動化備份
我自動化的另一個任務(wù)是備份。在開發(fā)的初期階段,我們沒有使用版本控制系統(tǒng)。但我們需要有一個機制來定期備份。因此,最好的方法是編寫一個
Shell 腳本,在執(zhí)行時將所有代碼文件復(fù)制到一個單獨的目錄中,將它們壓縮,并使用日期和時間作為后綴來上傳到
HDFS。我知道,這種方法不如使用版本控制系統(tǒng)那樣清晰,因為我們存儲了完整的文件,查找差異仍然需要使用像 diff
這樣的程序;但它總比沒有好。盡管我們最終沒有刪除代碼文件,但團隊確實刪除了存儲助手腳本的 bin
目錄!??!而且對于這個目錄,我沒有備份。我別無選擇,只能重新創(chuàng)建所有的腳本。
一旦建立了源代碼控制系統(tǒng),我很容易將備份腳本擴展到除了之前上傳到 HDFS 的方法之外,還可以將文件上傳到版本控制系統(tǒng)。
總結(jié)
如今,像 Python、Spark、Scala 和 Java 這樣的編程語言很受歡迎,因為它們用于開發(fā)與人工智能和機器學(xué)習(xí)相關(guān)的應(yīng)用程序。盡管與 Shell 相比,這些語言更強大,但“不起眼”的 Shell 提供了一個即用即得的平臺,讓我們能夠創(chuàng)建輔助腳本來簡化我們的日常任務(wù)。Shell 是相當(dāng)強大的,尤其是因為我們可以結(jié)合操作系統(tǒng)上安裝的所有應(yīng)用程序的功能。正如我在我的項目中發(fā)現(xiàn)的那樣,即使經(jīng)過了幾十年,Shell 腳本仍然非常強大。我希望我已經(jīng)說服你嘗試一下了。
最后一個例子
Shell 腳本確實非常方便??紤]以下命令:
spark3-submit --queue pyspark --conf "spark.yarn.principal=abcd@abcd.com" --conf "spark.yarn.keytab=/keytabs/abcd.keytab" --jars /opt/custom_jars/abcd_1.jar --deploy-mode cluster --master yarn $*
我們要求在執(zhí)行 Python/Spark 應(yīng)用程序時使用此命令?,F(xiàn)在想象一下,這個命令必須每天被一個由 40 個人組成的團隊多次使用。大多數(shù)人會在記事本中復(fù)制這個命令,每次需要使用時,會將其從記事本中復(fù)制并粘貼到終端中。如果復(fù)制粘貼過程中出現(xiàn)錯誤怎么辦?如果有人錯誤使用了參數(shù)怎么辦?我們?nèi)绾握{(diào)試使用的是哪個命令?查看歷史記錄并沒有太多幫助。
為了讓團隊能夠簡單地執(zhí)行 Python/Spark 應(yīng)用程序,我們可以創(chuàng)建一個 Bash Shell 腳本,如下所示:
#!/bin/bash
SERVICE_PRINCIPAL=abcd@abcd.com
KEYTAB_PATH=/keytabs/abcd.keytab
MY_JARS=/opt/custom_jars/abcd_1.jar
MAX_RETRIES=128
QUEUE=pyspark
MASTER=yarn
MODE=cluster
spark3-submit --queue ${QUEUE} --conf "spark.yarn.principal=${SERVICE_PRINCIPAL}" --conf "spark.yarn.keytab=${KEYTAB_PATH}" --jars ${MY_JARS} --deploy-mode ${MODE} --master ${MASTER} $*
這展示了一個 Shell 腳本的強大之處,讓我們的生活變得簡單。根據(jù)你的需求,你可以嘗試更多的命令和腳本,并進一步探索。