Bash One-Liners Explained 是一系列介紹 Bash 命令技巧的文章,由國外牛人 Peteris Krumins 撰寫。憑藉紮實的功底和豐富的經驗,作者總結了許多快速解決問題的技巧,並且每一條都只要用簡潔的一行 Bash 命令就可以完成,同時每一行命令文中都給出了非常詳盡的解釋。
Peteris Krumins 是一位高產的博主,在他的部落格上有很多非常精彩的文章,推薦大家有機會都可以去好好讀一讀。例如,大家耳熟能詳的 Awk One-Liners Explained、Sed One-Liners Explained 等等。後者我也曾經在部落格上分享過一篇筆記。
回到正題,雖然這一系列文章不難,但是還是可以從中學到很多細節的知識,相信這些肯定會對許多初學者有所幫助,所以我打算將這一系列翻譯成中文,分享給大家。為了同原文保持一致,這一系列文章最終會分成以下五篇:
- Bash One-Liners Explained 譯文(一): 檔案處理;
- Bash One-Liners Explained 譯文(二): 操作字串;
- Bash One-Liners Explained 譯文(三): 漫談重定向;
- Bash One-Liners Explained 譯文(四): 歷史命令;
- Bash One-Liners Explained 譯文(五): 命令列跳轉;
本系列的文章同其它系列一樣,最終都可以在連載頁面找到,有興趣的同學可以隨意翻翻,看看有沒有一些對你有價值的文章,大家一起交流學習。
1. 清空檔案內容
1 |
$ > file |
這一行命令用到了輸出重定向操作符>
。輸出重定向發生時,檔案會被開啟準備寫入。如果此時檔案不存在則先建立,存在則將其大小擷取為0。這裡我們並沒有重定向寫任何內容到檔案中,所以檔案依然保持為空。
如果你想替換檔案的內容,或者建立一個包含指定內容的檔案,可以執行下面的命令:
1 |
$ echo "some string" > file |
2. 追加內容到檔案
1 |
$ echo "foo bar baz" >> file |
這一行命令用到了另外一個輸出重定向操作符>>
,該操作符將內容追加到檔案。同樣地,如果檔案不存在則先建立它。追加的內容之後,緊跟著換行符。如果你不想要追加換行符,在執行echo
命令時可以指定-n
選項:
1 |
$ echo -n "foo bar baz" >> file |
3. 讀取檔案的首行並賦值給變數
1 |
$ read -r line < file |
這一行命令用到了 Bash 的內建命令read
,和輸入重定向操作符<
。read
命令從標準輸入中讀取一行,並將內容儲存到變數line
中。在這裡,-r
選項保證讀入的內容是原始的內容,意味著反斜槓轉義的行為不會發生。輸入重定向操作符< file
開啟並讀取檔案file
,然後將它作為read
命令的標準輸入。
記住,read
命令會刪除包含在IFS
變數中出現的所有字元,IFS 的全稱是 Internal Field Separator,Bash 根據 IFS 中定義的字元來分隔單詞。在這裡,read
命令讀入的行被分隔成多個單詞。預設情況下,IFS
包含空格,製表符和回車,這意味著開頭和結尾的空格和製表符都會被刪除。如果你想保留這些符號,可以通過設定IFS
為空來完成:
1 |
$ IFS= read -r line < file |
IFS 的變化僅會影響當前的命令,這行命令可以保證讀入原始的首行內容到變數line
中,同時行首與行尾的空白字元被保留。
另外一種讀取檔案首行內容,並賦值給變數的方法是:
1 |
$ line=$(head -1 file) |
這裡用到了命令替換操作符$(...)
,它執行括號裡的命令並且將輸出返回。 這個例子中,命令是head -1 file
,輸出的內容是檔案的首行。輸入然後通過等號賦值給變數line
。$(...)
的等價寫法是
,所以也可以換成下面這樣:...
1 |
$ line=`head -1 file` |
不過,在 Bash 中$(...)
用法更加推薦,因為它看起來更加整潔,並且容易巢狀使用。
4. 依次讀入檔案每一行
1 2 3 |
$ while read -r line; do # do something with $line done < file |
這是一種正確的讀取檔案內容的做法,read
命令放在while
迴圈中。當read
命令遇到檔案結尾時(EOF),它會返回一個正值,導致迴圈判斷失敗終止。
記住,read
命令會刪除首尾多餘的空白字元,所以如果你想保留,請設定 IFS 為空值:
1 2 3 |
$ while IFS= read -r line; do # do something with $line done < file |
如果你不想將< file
放在最後,可以通過管道將檔案的內容輸入到 while 迴圈中:
1 2 3 |
$ cat file | while IFS= read -r line; do # do something with $line done |
5. 隨機讀取一行並賦值給變數
1 |
$ read -r random_line < <(shuf file) |
Bash 中並沒有提供一種直接的方法來隨機讀取檔案的某一行內容,所以這裡需要利用外部程式。在最新的一些 Linux 系統上,GNU Coreutils 包中提供的shuf
命令可以滿足我們的需求。
這一行命令中用到了程式替換(process substitution)操作符<(...)
。程式替換操作會建立一個匿名的管道檔案,並將程式命令的標準輸出連線到管道的寫一端。然後 Bash 開始執行程式替換中的命令,然後將整個程式替換的表示式替換成匿名管道的檔名。
當 Bash 看到<(shuf file)
時,它首先開啟一個特殊的檔案/dev/fd/n
,這裡的n
是一個空閒的檔案描述符,然後執行shuf file
命令,將標準輸出連線到/dev/fd/n
,並且替換<(shuf file)
為/dev/fd/n
,因此實際的命令會變成:
1 |
$ read -r random_line < /dev/fd/n |
結果會讀取洗牌後的檔案的第一行內容。
另外一種做法是,使用 GNU sort 命令,它提供的-R
選項可以隨機排序檔案:
1 |
$ read -r random_line < <(sort -R file |
或者,同前面一樣,將結果賦值給變數:
1 |
$ random_line=$(sort -R file | head -1) |
這裡,我們首先通過sort -R
隨機排序檔案,然後通過head -1
讀取檔案的第一行。
6. 讀取檔案首行前三個欄位並賦值給變數
1 2 3 |
$ while read -r field1 field2 field3 throwaway; do # do something with $field1, $field2, and $field3 done < file |
如果在read
命令中指定多個變數名,它會將讀入的內容分隔成多個欄位,然後依次賦值給對應的變數,第一個欄位賦值給第一個變數,第二個欄位賦值給第二個變數,等等,最後將剩餘的所有欄位賦值給最後一個變數。這也是為什麼,在上面的例子中,我們加了一個throwaway
變數,否則的話,當檔案的一行大於三個欄位時,第三個變數的內容會包含所有剩餘的欄位。
有時候,為了書寫方便,可以簡單地用_
來替換throwaway
變數:
1 2 3 |
$ while read -r field1 field2 field3 _; do # do something with $field1, $field2, and $field3 done < file |
又或者,如果你的檔案確實只有三個欄位,那可以忽略它:
1 2 3 |
$ while read -r field1 field2 field3; do # do something with $field1, $field2, and $field3 done < file |
下面是一個例子,假如你想知道一個檔案到底包含多少行,多少個單詞以及多少個位元組。當你執行wc
命令時,你會得到3個數字加上檔名,檔名在最後:
1 2 3 4 5 6 7 8 9 |
$ 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
命令將這些資訊儲存到變數中:
1 2 3 4 5 6 7 8 |
$ read lines words chars _ < <(wc file-with-5-lines) $ echo $lines 5 $ echo $words 10 $ echo $chars 20 |
類似地,你也可以使用 here-strings 將字串分隔並儲存到變數中。假設你有一個字串變數$info
,內容為"20 packets in 10 seconds"
,然後你想要將從中獲取20
和10
。在不久之前,我是這樣來完成的:
1 2 |
$ packets=$(echo $info | awk '{ print $1 }') $ time=$(echo $info | awk '{ print $4 }') |
然而,得益於read
命令的強大和對 Bash 的瞭解,我們可以這樣做:
1 |
$ read packets _ _ time _ <<< "$info" |
這裡,<<<
就是 here-string 的語法,它允許你直接傳遞字串給標準輸入。
7. 儲存檔案的大小到變數
1 |
$ size=$(wc -c < file) |
這一行命令中用到了第3點中介紹的命令替換操作$(...)
,它執行裡面的命令並將結果獲取回來。在這個例子中,命令是wc -c < file
,它輸出檔案的位元組數。這個結果最終會賦值給變數size
。
8. 從檔案路徑中獲取檔名
假設,你有一個檔案,它的路徑為/path/to/file.ext
,然後你要從中獲取檔名,在這裡是file.ext
。你要怎麼做? 一個好的方法是通過引數展開(parameter expansion)功能:
1 |
$ filename=${path##*/} |
這一行命令使用了引數展開的語法:${var##pattern}
,它從$var
字串開始處開始匹配pattern
。如果能夠匹配成功,將最長匹配的內容刪除後再返回。
在這個例子中,匹配的模式是*/
,它嘗試匹配/path/to/file.ext
的開始部分,正如前面所說,這裡是貪婪匹配,所以它能夠匹配到最後一個斜槓為止,即匹配的內容是/path/to/
。所以當把匹配的內容刪除後,返回的內容就是檔名file.ext
。
9. 從檔案路徑中獲取目錄名
和上面一樣類似,這次你要從路徑/path/to/file.txt
中獲取目錄名/path/to
。你可以繼續通過引數展開功能來完成這個任務:
1 |
$ dirname=${path%/*} |
這次的用法是${var%pattern}
,它從$var
的結尾處匹配/*
。如果能夠成功匹配,將最短匹配的內容刪除再返回。
在這個例子中,匹配的模式是/*
,它能夠匹配/file.ext
部分,刪除這部分內容後返回的就是目錄名稱。
10. 快速拷貝檔案
假設你要將檔案/path/to/fil
拷貝到/path/to/file_copy
,一般情況下,大多數人會這麼來寫:
1 |
$ cp /path/to/file /path/to/file_copy |
不過,你可以利用括號展開(brace expansion){...}
功能:
1 |
$ cp /path/to/file{,_copy} |
括號展開可以生成任意字串的組合,在這個例子中,/path/to/file{,_copy}
最終生成/path/to/file /path/to/file_copy
。所以上面這行命令最終髮型成:
1 |
$ cp /path/to/file /path/to/file_copy |
類似地,你可以執行下面的命令快速的移動檔案:
1 |
$ mv /path/to/file{,_old} |
這行命令展開後就變成了:
1 |
$ mv /path/to/file /path/to/file_old |
本文完