如何在 Bash 中使用迴圈

2020-04-08 22:57:00

使用迴圈和查詢命令批次自動對多個檔案進行一系列的操作。

人們希望學習批次處理命令的一個普遍原因是要得到批次處理強大的功能。如果你希望批次的對檔案執行一些指令,構造一個可以重複執行在那些檔案上的命令就是一種方法。在程式設計術語中,這被稱作執行控制for 迴圈就是其中最常見的一種。

for 迴圈可以詳細描述你希望計算機對你指定的每個資料物件(比如說檔案)所進行的操作。

一般的迴圈

使用迴圈的一個簡單例子是對一組檔案進行分析。這個迴圈可能沒什麼用,但是這是一個安全的證明自己有能力獨立處理資料夾裡每一個檔案的方法。首先,建立一個資料夾然後拷貝一些檔案(例如 JPEG、PNG 等類似的檔案)至資料夾中生成一個測試環境。你可以通過檔案管理器或者終端來完成建立資料夾和拷貝檔案的操作:

$ mkdir example$ cp ~/Pictures/vacation/*.{png,jpg} example

切換到你剛建立的那個新資料夾,然後列出檔案並確認這個測試環境是你需要的:

$ cd example$ ls -1cat.jpgdesign_maori.pngotago.jpgwaterfall.png

在迴圈中逐一遍歷檔案的語法是:首先宣告一個變數(例如使用 f 代表檔案),然後定義一個你希望用變數回圈的資料集。在這種情況下,使用 * 萬用字元來遍歷當前資料夾下的所有檔案(萬用字元 * 匹配所有檔案)。然後使用一個分號(;)來結束這個語句。

$ for f in * ;

取決於你個人的喜好,你可以選擇在這裡按下確認鍵。在語法完成前,shell 是不會嘗試執行這個迴圈的。

接下來,定義你想在每次迴圈中進行的操作。簡單起見,使用 file 命令來得到 f 變數(使用 $ 告訴 shell 使用這個變數的值,無論這個變數現在儲存著什麼)所儲存著的檔案的各種資訊:

do file $f ;

使用另一個分號結束這一行,然後關閉這個迴圈:

done

按下確認鍵啟動 shell 對當前資料夾下所有東西的遍歷。for 迴圈將會一個一個的將檔案分配給變數 f 並且執行你的命令:

$ for f in * ; do> file $f ;> donecat.jpg: JPEG image data, EXIF standard 2.2design_maori.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlacedotago.jpg: JPEG image data, EXIF standard 2.2waterfall.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced

你也可以用這種形式書寫命令:

$ for f in *; do file $f; donecat.jpg: JPEG image data, EXIF standard 2.2design_maori.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlacedotago.jpg: JPEG image data, EXIF standard 2.2waterfall.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced

對你的 shell 來說,多行和單行的格式沒有什麼區別,並且會輸出完全一樣的結果。

一個實用的例子

下面是一個迴圈在日常使用中的實用案例。假如你擁有一堆假期拍的照片想要發給你的朋友。但你的照片太大了,無法通過電子郵件傳送,上傳到圖片分享服務也不方便。因此你想為你的照片建立小型的 web 版本,但是你不希望花費太多時間在一個一個的壓縮圖片體積上。

首先,在你的 Linux、BSD 或者 Mac 上使用包管理器安裝 ImageMagick 命令。例如,在 Fedora 和 RHEL 上:

$ sudo dnf install ImageMagick

在 Ubuntu 和 Debian 上:

$ sudo apt install ImageMagick

在 BSD 上,使用 ports 或者 pkgsrc 安裝。在 Mac 上,使用 Homebrew 或者 MacPorts 安裝。

在你安裝了 ImageMagick 之後,你就擁有一系列可以用來操作圖片的新命令了。

為你將要建立的檔案建立一個目標資料夾:

$ mkdir tmp

使用下面的迴圈可以將每張圖片減小至原來大小的 33%。

$ for f in * ; do convert $f -scale 33% tmp/$f ; done

然後就可以在 tmp 資料夾中看到已經縮小了的照片了。

你可以在迴圈體中使用任意數量的命令,因此如果你需要對一批檔案進行複雜的操作,可以將你的命令放在一個 for 迴圈的 dodone 語句之間。例如,假設你希望將所有處理過的圖片拷貝至你的網站所託管的圖片資料夾並且在本地系統移除這些檔案:

$ for f in * ; do  convert $f -scale 33% tmp/$f  scp -i seth_web tmp/$f [email protected]:~/public_html  trash tmp/$f ;done

你的計算機會對 for 迴圈中處理的每一個檔案自動的執行 3 條命令。這意味著假如你僅僅處理 10 張圖片,也會省下輸入 30 條指令和更多的時間。

限制你的迴圈

一個迴圈常常不需要處理所有檔案。在範例資料夾中,你可能需要處理的只是 JPEG 檔案:

$ for f in *.jpg ; do convert $f -scale 33% tmp/$f ; done$ ls -m tmpcat.jpg, otago.jpg

或者,你希望重複特定次數的某個操作而不僅僅只處理檔案。for 迴圈的變數的值是被你賦給它的(不管何種型別的)資料所決定的,所以你可以建立一個迴圈遍歷數位而不只是檔案:

$ for n in {0..4}; do echo $n ; done01234

更多迴圈

現在你了解的知識已經足夠用來建立自己的迴圈體了。直到你對迴圈非常熟悉之前,盡可能的在需要處理的檔案的副本上進行操作。使用內建的保護措施可以預防損壞自己的資料和製造不可複現的錯誤,例如偶然將一個資料夾下的所有檔案重新命名為同一個名字,就可能會導致他們的相互覆蓋。

更進一步的 for 迴圈話題,請繼續閱讀。

不是所有的 shell 都是 Bash

關鍵字 for 是內建在 Bash shell 中的。許多類似的 shell 會使用和 Bash 同樣的關鍵字和語法,但是也有某些 shell ,比如 tcsh,使用不同的關鍵字,例如 foreach

tcsh 的語法與 Bash 類似,但是它更為嚴格。例如在下面的例子中,不要在你的終端的第 2、3 行鍵入 foreach? 。它只是提示你仍處在構建迴圈的過程中。

$ foreach f (*)foreach? file $fforeach? endcat.jpg: JPEG image data, EXIF standard 2.2design_maori.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlacedotago.jpg: JPEG image data, EXIF standard 2.2waterfall.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced

在 tcsh 中,foreachend 都必須單獨的在一行中出現。因此你不能像 Bash 或者其他類似的 shell 一樣只使用一行命令建立一個 for 迴圈。

for 迴圈與 find 命令

理論上,你可能會用到不支援 for 迴圈的 shell,或者你只是更想使用其他命令的一些特性來完成和迴圈一樣的工作。

使用 find 命令是另一個實現 for 迴圈功能的途徑。這個命令提供了多種方法來定義迴圈中包含哪些檔案的範圍以及並行處理的選項。

find 命令顧名思義就是幫助你查詢儲存在硬碟裡的檔案。它的用法很簡單:提供一個你希望它查詢的位置的路徑,接著 find 就會查詢這個路徑下面的所有檔案和資料夾。

$ find .../cat.jpg./design_maori.png./otago.jpg./waterfall.png

你可以通過新增名稱的某些部分來過濾搜尋結果:

$ find . -name "*jpg"./cat.jpg./otago.jpg

find 命令非常好的地方在於你可以通過 -exec 引數標誌將它查詢到的每一個檔案放入迴圈中。例如,只對存放在你的 example 資料夾下的 PNG 圖片進行體積壓縮操作:

$ find . -name "*png" -exec convert {} -scale 33% tmp/{} \;$ ls -m tmpdesign_maori.png, waterfall.png

-exec 短語中,括號 {} 表示的是 find 正在處理的條目(換句話說,每一個被找到的以 PNG 結尾的檔案)。-exec 短語必須使用分號結尾,但是 Bash 中常常也會使用分號。為了解決這個二義性問題,你的 結束符 可以使用反斜槓加上一個分號(\;),使得 find 命令可以知道這個結束符是用來標識自己結束使用的。

find 命令的操作非常棒,某些情況下它甚至可以表現得更棒。比如說,在一個新的進程中使用同一條命令查詢 PNG 檔案,你可能就會得到一些錯誤資訊:

$ find . -name "*png" -exec convert {} -flip -flop tmp/{} \;convert: unable to open image `tmp/./tmp/design_maori.png':No such file or directory @ error/blob.c/OpenBlob/2643....

看起來 find 不只是定位了當前資料夾(.)下的所有 PNG 檔案,還包括已經處理並且儲存到了 tmp 下的檔案。在一些情況下,你可能希望 find 查詢當前資料夾下再加上其子資料夾下的所有檔案。find 命令是一個功能強大的遞迴工具,特別體現在處理一些檔案結構複雜的情境下(比如用來放置存滿了音樂人音樂專輯的資料夾),同時你也可以使用 -maxdepth 選項來限制最大的遞迴深度。

只在當前資料夾下查詢 PNG 檔案(不包括子資料夾):

$ find . -maxdepth 1 -name "*png"

上一條命令的最大深度再加 1 就可以查詢和處理當前資料夾及下一級子資料夾下面的檔案:

$ find . -maxdepth 2 -name "*png"

find 命令預設是查詢每一級資料夾。

迴圈的樂趣與收益

你使用的迴圈越多,你就可以越多的省下時間和力氣,並且可以應對龐大的任務。雖然你只是一個使用者,但是通過使用迴圈,可以使你的計算機完成困難的任務。

你可以並且應該就像使用其他的命令一樣使用迴圈。在你需要重複處理單個或多個檔案時,盡可能的使用這個命令。無論如何,這也算是一項需要被嚴肅對待的程式設計活動,因此如果你需要在一些檔案上完成複雜的任務,你應該多花點時間在規劃自己的工作流上面。如果你可以在一份檔案上完成你的工作,接下來將操作包裝進 for 迴圈裡就相對簡單了,這裡面唯一的“程式設計”的需要只是理解變數是如何工作的並且進行充分的規劃工作將已處理過的檔案和未處理過的檔案分開。經過一段時間的練習,你就可以從一名 Linux 使用者升級成一位知道如何使用迴圈的 Linux 使用者,所以開始讓計算機為你工作吧!