這是一篇網路上技術文章的翻譯,原文 是系列技術文章的第一篇。講解 Bash 命令列及技巧,譯者覺得對於入門 Linux 命令列有幫助,翻譯出來做為參考,學習它需勤動手練習。感興趣者可隨 原文連結 解讀其系列文章進行學習。
翻譯的系列文章列表:
- Bash 單命令列解釋,第一部分:檔案操作 (本篇)
我想使用 shell(外殼)程式超級快地操控系統,所以創作了名為 Bash One-Liners Explained 的系列文章。正如我創作的其它系列文章-- Awk One-Liners Explained,Sed One-Liners Explained 和 Perl One-Liners Explained 一樣。若這個 bash 系列文章完成後,我將放出其同名的電子書。我的電子書 有 pdf 格式及方便在手機上看的格式(mobi 和 epub)。同時,像 perl1line.txt 一樣,也有純文字 txt 格式的文件。
在這個系列中,我將利用 bash 最佳實踐,多變的 bash 方言和 bash 命令技巧,僅展示利用 bash 內建命令和外部程式命令構建命令流完成各種各樣的任務需求。
也可參考我其它的高效使用 bash 的系列文章:
- Working Productively in Bash's Emacs Command Line Editing Mode (comes with a cheat sheet)
- Working Productively in Bash's Vi Command Line Editing Mode (comes with a cheat sheet)
- The Definitive Guide to Bash Command Line History (comes with a cheat sheet)
開始學習
第一部分:檔案操作
1. 清空檔案(清除檔案大小為 0)
$ > file
這行命令使用輸出重定向操作符 >
。輸出重定向造成檔案被開啟並寫入。如果檔案不存在就建立它,若存在就清除檔案大小為 0。由於重定向沒給檔案任何內容,結果就清空了檔案。
如果想用一些字串文字替換檔案裡的內容,可用如下命令:
$ echo "some string" > file
這將 some string
字串寫入檔案
2. 追加字串寫入檔案
$ echo "foo bar baz" >> file
這行命令使用另一個輸出重定向操作符 >>
,意思追加內容到檔案。如果檔案不存在就建立它並寫入。追加 的內容放到檔案尾並放到新行(另起一行)。若不想另起一行,可以使用 echo
命令選項 -n
,形如 echo -n
命令:
$ echo -n "foo bar baz" >> file
3. 讀取檔案首行並賦值到一個變數
$ read -r line < file
本命令使用內建命令 read
和重定向符 <
。
解釋: read
命令從標準輸入讀取一行文字到 line
變數。-r
命令選項意味著輸入保留原始(raw)--意思是字串中的轉義字元反斜槓(\) 保持原樣。重定向命令 < file
意味著標準輸入被 file
代替 -- 從 file
檔案中讀取資料。
read
命令讀入字串會按照 IFS
環境變數設定,刪除讀取的一行字串中行首、行尾的分隔符。IFS 表示 間隔符(Internal Field Separator) 的含義。用於命令列擴充套件分隔單詞和斷行,預設 IFS 設定為 空格(space)、製表符(tab)、回車換行(newline \n) 。如果想保留這些,可利用 IFS
即時清除設定命令:
$ IFS= read -r line < file
上面命令僅限當前命令,並不會改變 IFS 的設定。只用於將檔案中的一行文字真正原樣讀出。
另外的從檔案中讀出第一行的方法:
$ line=$(head -1 file)
上面方法使用命令執行替換 操作符 $(...)
,它執行括號中 ...
的命令返回其輸出。本例中,命令 head -1 file
輸出檔案的第一行,之後賦值給 line
變數。 使用 $(...)
等同於 `...` ,故也可寫成:
$ line=`head -1 file`
然而,$(...)
是優選方法,它簡潔,易於巢狀。
4. 一行行地讀取檔案
$ while read -r line; do
# do something with $line
done < file
這是唯一正確的一行行地讀取檔案的方法。它將 read
命令放到 while
迴圈中,當 read
命令遇到檔案結束,它返回一個非零正數(程式碼失敗),迴圈結束。
記住 read
命令會刪除前導、結尾的分隔符(空格,tab,回車),若想保留分隔符,使用 IFS
即時清除設定命令:
$ while IFS= read -r line; do
# do something with $line
done < file
若不喜歡迴圈語句尾加 < file
重定向操作,也可使用管道命令:
$ cat file | while IFS= read -r line; do
# do something with $line
done
5. 從檔案中隨機讀取一行並賦值變數
$ read -r random_line < <(shuf file)
這命令對於 bash ,不是一個明晰的從檔案隨機讀取行的方法。因為它需要藉助某些外部程式的幫助。在一臺執行現代 Linux 的機器上使用來自 GNU 的核心工具 shut
命令。
這條命令使用程式執行替換操作符 <(...)
,這個程式執行替換操作建立匿名管道並連線程式執行的輸出部分到某個命名管道的寫入部分,然後 bash 執行這個程式,這樣就把執行程式的匿名管道替換成了命名管道(用裝置檔案描述符表示)。
當 bash 解析 <(shuf file)
命令部分時,它開啟一個特殊的裝置檔案 /dev/fd/n
,其中 n
是一個未使用的檔案描述符,然後執行 shut file
程式並將它的輸出連線到 /dev/fd/n
,這樣 <(shut file)
被替換成 < /dev/fd/n
。實際效果如下:
$ read -r random_line < /dev/fd/n
這將從這個被行隨機化過的檔案中讀取第一行。
下面是藉助 GNU 的 sort
命令完成同樣目標的另一解決方案。GNU 的 sort
命令通過 -R
命令選項隨機化輸入。
$ read -r random_line < <(sort -R file)
另外的獲取檔案中隨機一行並賦值變數的方法:
$ random_line=$(sort -R file | head -1)
上面命令通過 sort -R
命令隨機化檔案後,執行 head -1
取出其第一行。
6. 從檔案讀取一行並分割前3個單詞賦值給3個變數
$ while read -r field1 field2 field3 throwaway; do
# do something with $field1, $field2, and $field3
done < file
如果在 read
命令中指定超過一個的變數名,將會把輸入字串分割成符合變數個數的欄位(也稱域,依據 IFS
設定的分隔符進行分割。預設是空格、tab、回車),之後,把第1個欄位分配給第1個變數,把第2個欄位分配給第2個變數,以此類推。但是,最後一個欄位是字串剩下的全部。這就是我們最後使用一個不用的變數(throwaway)的原因,若沒有它,也許 field3
中並不是我們期望的值。
經常我們會用 _
更短的形式替換 throwaway
做為棄用的變數名:
$ while read -r field1 field2 field3 _; do
# do something with $field1, $field2, and $field3
done < file
當然,若你能保證字串中只包含3個欄位,你完全可以不安排這個無用的變數名:
$ while read -r field1 field2 field3; do
# do something with $field1, $field2, and $field3
done < file
這裡有個例子。我們知道,當想知道一個檔案內容有多少行、多少單詞,多少字元時,在檔案上執行 wc
命令,將輸出這3個數值和檔名,如下所示:
$ cat file-with-5-lines
x 1
x 2
x 3
x 4
x 5
$ wc file-with-5-lines
5 10 20 file-with-5-lines
因此,這個檔案有5行,10個單詞和20個字元(空格、回車都算)。我們可使用 read
命令獲取這些數值並放到變數中。演示如下:
$ read lines words chars _ < <(wc file-with-5-lines)
$ echo $lines
5
$ echo $words
10
$ echo $chars
20
類似地,可以使用 即入字串(here-strings) 將字串分割成多個欄位賦值給變數。假設有個字串 "20 packets in 10 seconds" 儲存在 $info
變數中,現將 20 和 10 取出放到2個變數中。不久前這個任務我寫過:
$ packets=$(echo $info | awk '{ print $1 }')
$ duration=$(echo $info | awk '{ print $4 }')
然而,若善用 read
命令和 bash 的能力,使用如下一條簡單命令可達成目標:
$ read packets _ _ duration _ <<< "$info"
這裡的 <<<
是 bash 中的一種重定向機制,被稱為 即入字串(here-strings)重定向 操作符,用於直接將一個字串重定向到標準輸入傳遞給命令。
7. 獲取檔案大小並賦值變數
$ size=$(wc -c < file)
上面的一行命令使用命令執行替換操作符 $(...)
(在第3段--3.讀取檔案首行並賦值到一個變數--中已講過)。本命令中將 wc -c < file
執行結果,即得出的 file 的大小賦值給變數 size 。(譯註:思考並實驗這裡為什麼使用 wc -c < file
而不使用 wc -c file
?)
8. 從全路徑字串中取出檔名
讓我們看個例子,假設有個 "/path/to/file.txt" 字串表示檔案的全路徑,只想取出其中的檔名 file.txt ,該如何做呢? 一個好的解決方案使用 bash 外殼程式的 引數展開(parameter expansion) 機制。演示如下:
$ pvar = "/path/to/file.txt"
$ filename=${pvar##*/}
如上命令,使用形如 ${varname##pattern}
的引數展開 操作符。這個展開操作試圖從$varname
變數表示的字串開始按 pattern 進行模式匹配,若匹配成功,將從 $varname 字串刪除掉匹配的子字串後的剩餘部分返回。(刪除匹配)
上面例子的情況 */
模式將在 "/path/to/file.txt" 中 從頭到尾 匹配任意字元後跟 '/' 的模式,由於 */
是貪心匹配,故匹配結果是 /path/to/ 。按照展開邏輯,表示式將返回 刪除匹配 的子串,所以,變數 $filename 將等於 file.txt 。(其中 ## 表示 從頭到尾 開始模式匹配)
9. 從全路徑字串中取出目錄名
與前一任務命令相似,這次讓我們從 "/path/to/file.txt" 中取出目錄名 /path/to/ 子串。同樣使用 引數展開 操作,命令如下:
$ pvar = "/path/to/file.txt"
$ dirname=${pvar%/*}
這次 ${varname%pattern}
的引數展開 操作是 從尾到頭 的匹配模式(其中 % 表示 從尾到頭 開始模式匹配)。
例子中,,模式為 /* 。從尾到頭匹配後,匹配結果為 /file.txt 。將匹配刪除後的結果為 /path/to 。
10. 快速建立檔案副本
讓我們看一下將 /path/to/file 表示的檔案,在同目錄下拷貝一個副本命令。通常你會寫成如下命令:
$ cp /path/to/file /path/to/file_copy
然而,你可以使用 大括號展開 機制,命令寫成如下簡短形式:
$ cp /path/to/file{,_copy}
大括號展開 機制是將大括號中的每個條目依次展開(列舉每個條目)。例子中 /path/to/file{,_copy}
將會被展開成 /path/to/file /path/to/file_copy
,整個命令將變成 cp /path/to/file /path/to/file_copy
。
相似地,可以快速移動(改名)一個檔案,如下命令:
$ mv /path/to/file{,_old}
命令展開成 mv /path/to/file /path/to/file_old
。
歡迎指正
享受文中所說的方法和技巧使用的樂趣吧,並且讓我知道您的想法。也許我漏掉了什麼,非常樂意收到您的指正。