怎樣用 Bash 程式設計:迴圈

2019-12-26 11:15:00

本文是 Bash 程式設計系列三篇中的最後一篇,來學習使用迴圈執行迭代的操作。

Bash 是一種強大的用於命令列和 shell 指令碼的程式語言。本系列的三部分都是基於我的三集 Linux 自學課程 寫的,探索怎麼用 CLI 進行 bash 程式設計。

本系列的 討論了 bash 程式設計的一些簡單命令列操作,如使用變數和控制操作符。 探討了檔案、字串、數位等型別和各種各樣在執行流中提供控制邏輯的的邏輯運算子,還有 bash 中不同種類的擴充套件。本文是第三篇(也是最後一篇),意在考察在各種疊代的操作中使用迴圈以及怎麼合理控制迴圈。

迴圈

我使用過的所有程式語言都至少有兩種迴圈結構來用來執行重複的操作。我經常使用 for 迴圈,然而我發現 whileuntil 迴圈也很有用處。

for 迴圈

我的理解是,在 bash 中實現的 for 命令比大部分語言靈活,因為它可以處理非數位的值;與之形成對比的是,諸如標準 C 語言的 for 迴圈只能處理數位型別的值。

Bash 版的 for 命令基本的結構很簡單:

for Var in list1 ; do list2 ; done

解釋一下:“對於 list1 中的每一個值,把 $Var 設定為那個值,使用該值執行 list2 中的程式語句;list1 中的值都執行完後,整個迴圈結束,退出迴圈。” list1 中的值可以是一個簡單的顯式字串值,也可以是一個命令執行後的結果(`` 包含其內的命令執行的結果,本系列第二篇文章中有描述)。我經常使用這種結構。

要測試它,確認 ~/testdir 仍然是當前的工作目錄(PWD)。刪除目錄下所有東西,來看下這個顯式寫出值列表的 for 迴圈的簡單的範例。這個列表混合了字母和數位 — 但是不要忘了,在 bash 中所有的變數都是字串或者可以被當成字串來處理。

[student@studentvm1 testdir]$ rm *[student@studentvm1 testdir]$ for I in a b c d 1 2 3 4 ; do echo $I ; doneabcd1234

給變數賦予更有意義的名字,變成前面版本的進階版:

[student@studentvm1 testdir]$ for Dept in "Human Resources" Sales Finance "Information Technology" Engineering Administration Research ; do echo "Department $Dept" ; doneDepartment Human ResourcesDepartment SalesDepartment FinanceDepartment Information TechnologyDepartment EngineeringDepartment AdministrationDepartment Research

建立幾個目錄(建立時顯示一些處理資訊):

[student@studentvm1 testdir]$ for Dept in "Human Resources" Sales Finance "Information Technology" Engineering Administration Research ; do echo "Working on Department $Dept" ; mkdir "$Dept"  ; doneWorking on Department Human ResourcesWorking on Department SalesWorking on Department FinanceWorking on Department Information TechnologyWorking on Department EngineeringWorking on Department AdministrationWorking on Department Research[student@studentvm1 testdir]$ lltotal 28drwxrwxr-x 2 student student 4096 Apr  8 15:45  Administrationdrwxrwxr-x 2 student student 4096 Apr  8 15:45  Engineeringdrwxrwxr-x 2 student student 4096 Apr  8 15:45  Financedrwxrwxr-x 2 student student 4096 Apr  8 15:45 'Human Resources'drwxrwxr-x 2 student student 4096 Apr  8 15:45 'Information Technology'drwxrwxr-x 2 student student 4096 Apr  8 15:45  Researchdrwxrwxr-x 2 student student 4096 Apr  8 15:45  Sales

mkdir 語句中 $Dept 變數必須用引號包裹起來;否則名字中間有空格(如 Information Technology)會被當做兩個獨立的目錄處理。我一直信奉的一條實踐規則:所有的檔案和目錄都應該為一個單詞(中間沒有空格)。雖然大部分現代的作業系統可以處理名字中間有空格的情況,但是系統管理員需要花費額外的精力去確保指令碼和 CLI 程式能正確處理這些特例。(即使它們很煩人,也務必考慮它們,因為你永遠不知道將擁有哪些檔案。)

再次刪除 ~/testdir 下的所有東西 — 再執行一次下面的命令:

[student@studentvm1 testdir]$ rm -rf * ; lltotal 0[student@studentvm1 testdir]$ for Dept in Human-Resources Sales Finance Information-Technology Engineering Administration Research ; do echo "Working on Department $Dept" ; mkdir "$Dept"  ; doneWorking on Department Human-ResourcesWorking on Department SalesWorking on Department FinanceWorking on Department Information-TechnologyWorking on Department EngineeringWorking on Department AdministrationWorking on Department Research[student@studentvm1 testdir]$ lltotal 28drwxrwxr-x 2 student student 4096 Apr  8 15:52 Administrationdrwxrwxr-x 2 student student 4096 Apr  8 15:52 Engineeringdrwxrwxr-x 2 student student 4096 Apr  8 15:52 Financedrwxrwxr-x 2 student student 4096 Apr  8 15:52 Human-Resourcesdrwxrwxr-x 2 student student 4096 Apr  8 15:52 Information-Technologydrwxrwxr-x 2 student student 4096 Apr  8 15:52 Researchdrwxrwxr-x 2 student student 4096 Apr  8 15:52 Sales

假設現在有個需求,需要列出一台 Linux 機器上所有的 RPM 包並對每個包附上簡短的描述。我為北卡羅來納州工作的時候,曾經遇到過這種需求。由於當時開源尚未得到州政府的“批准”,而且我只在桌上型電腦上使用 Linux,對技術一竅不通的老闆(PHB)需要我列出我計算機上安裝的所有軟體,以便他們可以“批准”一個特例。

你怎麼實現它?有一種方法是,已知 rpm –qa 命令提供了 RPM 包的完整描述,包括了白痴老闆想要的東西:軟體名稱和概要描述。

讓我們一步步執行出最後的結果。首先,列出所有的 RPM 包:

[student@studentvm1 testdir]$ rpm -qaperl-HTTP-Message-6.18-3.fc29.noarchperl-IO-1.39-427.fc29.x86_64perl-Math-Complex-1.59-429.fc29.noarchlua-5.3.5-2.fc29.x86_64java-11-openjdk-headless-11.0.ea.28-2.fc29.x86_64util-linux-2.32.1-1.fc29.x86_64libreport-fedora-2.9.7-1.fc29.x86_64rpcbind-1.2.5-0.fc29.x86_64libsss_sudo-2.0.0-5.fc29.x86_64libfontenc-1.1.3-9.fc29.x86_64<snip>

sortuniq 命令對列表進行排序和列印去重後的結果(有些已安裝的 RPM 包具有相同的名字):

[student@studentvm1 testdir]$ rpm -qa | sort | uniqa2ps-4.14-39.fc29.x86_64aajohan-comfortaa-fonts-3.001-3.fc29.noarchabattis-cantarell-fonts-0.111-1.fc29.noarchabiword-3.0.2-13.fc29.x86_64abrt-2.11.0-1.fc29.x86_64abrt-addon-ccpp-2.11.0-1.fc29.x86_64abrt-addon-coredump-helper-2.11.0-1.fc29.x86_64abrt-addon-kerneloops-2.11.0-1.fc29.x86_64abrt-addon-pstoreoops-2.11.0-1.fc29.x86_64abrt-addon-vmcore-2.11.0-1.fc29.x86_64<snip>

以上命令得到了想要的 RPM 列表,因此你可以把這個列表作為一個迴圈的輸入資訊,迴圈最終會列印每個 RPM 包的詳細資訊:

[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done

這段程式碼產出了多餘的資訊。當迴圈結束後,下一步就是提取出白痴老闆需要的資訊。因此,新增一個 egrep 命令用來搜尋匹配 ^Name^Summary 的行。脫字元(^)表示行首,整個命令表示顯示所有以 Name 或 Summary 開頭的行。

[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done | egrep -i "^Name|^Summary"Name        : a2psSummary     : Converts text and other types of files to PostScriptName        : aajohan-comfortaa-fontsSummary     : Modern style true type fontName        : abattis-cantarell-fontsSummary     : Humanist sans serif fontName        : abiwordSummary     : Word processing programName        : abrtSummary     : Automatic bug detection and reporting tool<snip>

在上面的命令中你可以試試用 grep 代替 egrep ,你會發現用 grep 不能得到正確的結果。你也可以通過管道把命令結果用 less 過濾器來檢視。最終命令像這樣:

[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done | egrep -i "^Name|^Summary" > RPM-summary.txt

這個命令列程式用到了管道、重定向和 for 迴圈,這些全都在一行中。它把你的 CLI 程式的結果重定向到了一個檔案,這個檔案可以在郵件中使用或在其他地方作為輸入使用。

這個一次一步構建程式的過程讓你能看到每步的結果,以此來確保整個程式以你期望的流程進行且輸出你想要的結果。

白痴老闆最終收到了超過 1900 個不同的 RPM 包的清單,我嚴重懷疑根本就沒人讀過這個列表。我給了他們想要的東西,沒有從他們嘴裡聽到過任何關於 RPM 包的資訊。

其他迴圈

Bash 中還有兩種其他型別的迴圈結構:whileuntil 結構,兩者在語法和功能上都類似。這些迴圈結構的基礎語法很簡單:

while [ expression ] ; do list ; done

邏輯解釋:表示式(expression)結果為 true 時,執行程式語句 list。表示式結果為 false 時,退出迴圈。

until [ expression ] ; do list ; done

邏輯解釋:執行程式語句 list,直到表示式的結果為 true。當表示式結果為 true 時,退出迴圈。

While 迴圈

while 迴圈用於當邏輯表示式結果為 true 時執行一系列程式語句。假設你的 PWD 仍是 ~/testdir

最簡單的 while 迴圈形式是這個會一直執行下去的迴圈。下面格式的條件語句永遠以 true 作為返回。你也可以用簡單的 1 代替 true,結果一樣,但是這解釋了 true 表示式的用法。

[student@studentvm1 testdir]$ X=0 ; while [ true ] ; do echo $X ; X=$((X+1)) ; done | head0123456789[student@studentvm1 testdir]$

既然你已經學了 CLI 的各部分知識,那就讓它變得更有用處。首先,為了防止變數 $X 在前面的程式或 CLI 命令執行後有遺留的值,設定 $X 的值為 0。然後,因為邏輯表示式 [ true ] 的結果永遠是 1,即 true,在 dodone 中間的程式指令列表會一直執行 — 或者直到你按下 Ctrl+C 抑或傳送一個 2 號信號給程式。那些程式指令是算數擴充套件,用來列印變數 $X 當前的值並加 1.

系統管理員的 Linux 哲學》的信條之一是追求優雅,實現優雅的一種方式就是簡化。你可以用操作符 ++ 來簡化這個程式。在第一個例子中,變數當前的值被列印出來,然後變數的值增加了。可以在變數後加一個 ++ 來表示這個邏輯:

[student@studentvm1 ~]$ X=0 ; while [ true ] ; do echo $((X++)) ; done | head0123456789

現在刪掉程式最後的 | head 再執行一次。

在下面這個版本中,變數在值被列印之前就自增了。這是通過在變數之前新增 ++ 操作符實現的。你能看出區別嗎?

[student@studentvm1 ~]$ X=0 ; while [ true ] ; do echo $((++X)) ; done | head123456789

你已經把列印變數的值和自增簡化到了一條語句。類似 ++ 操作符,也有 -- 操作符。

你需要一個在迴圈到某個特定數位時終止迴圈的方法。把 true 表示式換成一個數位比較表示式來實現它。這裡有一個迴圈到 5 終止的程式。在下面的範例程式碼中,你可以看到 -le 是 “小於或等於” 的數位邏輯操作符。整個語句的意思:只要 $X 的值小於或等於 5,迴圈就一直執行。當 $X 增加到 6 時,迴圈終止。

[student@studentvm1 ~]$ X=0 ; while [ $X -le 5 ] ; do echo $((X++)) ; done012345[student@studentvm1 ~]$

Until 迴圈

until 命令非常像 while 命令。不同之處是,它直到邏輯表示式的值是 true 之前,會一直迴圈。看一下這種結構最簡單的格式:

[student@studentvm1 ~]$ X=0 ; until false  ; do echo $((X++)) ; done | head0123456789[student@studentvm1 ~]$

它用一個邏輯比較表示式來計數到一個特定的值:

[student@studentvm1 ~]$ X=0 ; until [ $X -eq 5 ]  ; do echo $((X++)) ; done01234[student@studentvm1 ~]$ X=0 ; until [ $X -eq 5 ]  ; do echo $((++X)) ; done12345[student@studentvm1 ~]$

總結

本系列探討了構建 Bash 命令列程式和 shell 指令碼的很多強大的工具。但是這僅僅是你能用 Bash 做的很多有意思的事中的冰山一角,接下來就看你的了。

我發現學習 Bash 程式設計最好的方法就是實踐。找一個需要多個 Bash 命令的簡單專案然後寫一個 CLI 程式。系統管理員們要做很多適合 CLI 程式設計的工作,因此我確信你很容易能找到自動化的任務。

很多年前,儘管我對其他的 Shell 語言和 Perl 很熟悉,但還是決定用 Bash 做所有系統管理員的自動化任務。我發現,有時稍微搜尋一下,我可以用 Bash 實現我需要的所有事情。