別催了,別催了,這篇文章我一次性把Shell的內容說完

語言: CN / TW / HK

Shell 搜尋與匹配

1、在檔案中查詢字串

grep 命令可以搜尋檔案,查詢指定的字串。

$ grep myvar *.c

在這個例子中,我們搜尋的檔案全都位於當前目錄下。因此,我們只使用了簡單的 shell 模式 *.c 來匹配以 .c 結束的檔案,並沒有在檔名前再新增路徑。

但並非所有待搜尋的檔案都老老實實地待在當前目錄下。但因為shell 並不在意你輸入多少路徑名,所以我們也可以這麼寫:

$ grep myvar ../lib/*.c ../server/*.c ../cmd/*.c */*.c

如果待搜尋的檔案不止一個,grep 會在輸出前加上檔名以及冒號,然後是該檔案中包含 grep 搜尋內容的文字。

grep 的第一個(非選項)引數可以是一個簡單的字串,也可以是更復雜的正則表示式(regexp)。正則表示式不同於 shell 的模式匹配,儘管兩者有時看起來差不多。

常見錯誤

忘記指定 grep 的輸入,例如 grep myvar。這種情況下,grep 會認為你要從 STDIN 提供輸入,而你以為它會讀取檔案,於是 grep 就乾等著,無所事事。

2、只顯示包含搜尋結果的檔名

你需要找出包含特定字串的檔案,但是不想看到其所在的文字行,只用輸出檔名即可,經常在線上為了搜尋配置檔案。

用 grep 的 -l 選項僅顯示檔名即可,如下:

$ grep -l myvar *.c
both.c
good.c
somio.c
$

如果在一個檔案中找到了多次匹配,grep 仍然只輸出該檔名一次。如果沒有找到匹配,則什麼都不輸出。

由於這些檔案包含了你要查詢的字串,如果想據此構建一個待處理

檔案的列表,選項 -l 就能派上用場了。將 grep 命令放進 $(),然後就可以在命令列上使用這些檔名了,如下:

rm -i $(grep -l 'This file is obsolete' * ) 

刪除包含字串“This file is obsolete”的檔案,我們給 rm 加上了 -i 選項,以便在刪除每個檔案前都先詢問你。

3、不區分大小寫搜尋

你想要在日誌檔案中不區分大小寫地搜尋字串(如“error”),以匹配該字串的所有出現。用 grep 的 -i 選項忽略大小寫,如下所示:

grep -i error logfile.log

不區分大小寫的搜尋能夠找出包含“ERROR”、“error”、“Error”的日誌訊息,“ErrOR”和“eRrOr”這樣的也不例外。該選項在查詢大小寫混合的單詞時尤其管用,或者對於查詢的內容無法確定大小時。

4、縮減搜尋結果

如果搜尋返回的結果不符合預期,其中包括許多並不需要的內容。將結果通過管道傳給 grep -v 並用表示式描述出你不想看到的內容。假設你想在日誌檔案中找出整個 12 月的日誌訊息。你知道日誌檔案用字母縮寫 Dec 代表 12 月,但不敢肯定總是如此,為了確保找出所有的日誌訊息,輸入下列命令:

grep -i dec logfile

得到的結果卻如下所示:

...
error on Jan 01: not a decimal number
error on Feb 13: base converted to Decimal
warning on Mar 22: using only decimal numbers
error on Dec 16 : the actual message you wanted
error on Jan 01: not a decimal number
...

一種快而糙的解決方案是,將第一次得到的結果通過管道傳給另一個grep,由後者過濾掉所有的“decimal”。

grep -i dec logfile | grep -vi decimal

將多個 grep 串聯在一起(因為前所未見、出乎意料的匹配會不斷出現),逐步過濾搜尋結果,直至滿意,這種做法並不鮮見。

-v 選項非常方便,你只需要記住該排除什麼就行了。

5、搜尋更復雜的模式

grep 中的正則表示式提供了更為強大的模式匹配功能,能夠滿足大部分需求。正則表示式描述了待匹配字串的模式。字母字元(或者對於 shell沒有特殊含義的其他字元)只匹配自身。“A”匹配 A,“B”匹配B,這沒什麼好說的。另一個重要的規則是按位置組合字母,如 AB匹配“AB”。這看起來也是顯而易見的。但是,正則表示式還定義了其他一些特殊字元,它們既可以單獨使用,也可以與其他字元結合,從而形成更為複雜的模式。

第一個特殊字元是點號(.),它可以匹配任意單個字元。因此,.... 可以匹配任意 4 個字元;A. 匹配“A”以及緊隨其後的任意單個字元;.A. 匹配任意單個字元,然後是“A”,接著是任意單個字元(未必和匹配到的第一個字元相同)。

第二個特殊字元時星號(**),匹配上一個字元的 0 次或多次出現,因此,A* 匹配 0 個或多個“A”字元,.* 匹配 0 個或多個任意字元(如“abcdefg”、“aaaabc”、“sdfgf ;lkjhj”,甚至是空行)。

那麼 ..* 是什麼意思?它匹配任意單個字元以及緊隨其後的 0 個或多個任意字元(也就是一個或多個字元,但不能是空行)。

如下所示,我們知道一行的某些單詞,我們想模糊匹配,如下操作:

grep -E "1.*22" 2.text  // 匹配1開頭,任意個字元後是22

結果如下:

1898090808098822:  

Shell 檔案查詢

1、查詢所有的txt檔案

檔案系統中到處都是 txt 檔案。你想將它們集中到一個位置。那麼我們該如何做呢?

find 命令可以找出符合要求的所有檔案並執行命令,將其移動到指定位置。例如:

find . -name '*.txt' -print -exec mv '{}'  /txts  \;

find 命令的語法和其他 Unix 命令不同,其選項並不是那種典型的連字元加上單字母,後面再跟上若干引數。find 命令的選項看起來像是簡短的單詞 1,依照邏輯順序出現,並描述要查詢哪些檔案以及如何處理找到的檔案(如果存在的話)。這種像單詞一樣的選項通常稱為謂詞(predicate)。

find 命令的第一個引數是待搜尋的目錄。典型用法是用點號(.)代表當前目錄,不過你也可以提供一個目錄列表,甚至通過指定根目錄(/)來搜尋整個檔案系統(只要許可權允許)。

示例中的第一個選項(謂詞 -name)指定了要搜尋的檔案模式。其語法和 bash 的模式匹配語法差不多,因此 *.txt 能夠匹配所有以“.txt”結尾的檔名。匹配該模式的檔案被認為返回的是真(true),接著將其交給下一個謂詞進行處理。

find 會遍歷檔案系統,將找到的檔名交給謂詞測試。如果謂詞返回真,就通過。如果返回假,則不再繼續往下進行,會接著處理下一個檔名。

謂詞 -print 很簡單。它總是返回真,同時會將檔名列印到標準輸出,因此,能在謂詞序列中通過測試而到達這一步的檔案都會輸出其名稱。如果不寫,預設會帶有這個謂詞。

-exec 就有點怪異了。到達這一步的檔名都會變成接下來要執行的命令的一部分。剩下一直到 ; 的這部分就是命令,其中的 {} 會被替換成已查詢到的檔名。因此,在上面的例子中,如果 find 在./txt/jazz 子目錄中找到名為 1.txt 的檔案,那麼要執行的命令就會是:

mv ./txt/jazz/1.txt  /txts

所有匹配指定模式的檔案都會執行命令。如果找到的檔案數量眾多,那麼命令的執行次數自然也不會少。

2、提升已找到檔案的處理速度

按照上面的例子,find命令會為每個名字符合要求的檔案執行命令,但是當檔案過多時命令自然會很慢,那麼我們如何提高速度呢?

xargs 命令從標準輸入中接收以空白字元分隔(指定 -0 時除外)的檔名,然後對儘可能多的檔案(略微少於系統的 ARG_MAX 值,參見 15.13 節)執行指定命令。由於呼叫其他命令會帶來不小的開銷,因此使用 xargs 可以顯著提升操作速度,因為它能夠儘量減少命令的呼叫次數,而不是每個檔案都呼叫。如下所示:

find . -name '*.txt' -print  | xargs  mv '{}'  /txts;

3、查詢檔案時不區分大小寫

有些 TXT 檔案的副檔名是 .TXT,而不是 .txt。查詢時該如何兼顧兩者?

用 -iname 謂詞(如果使用的 find 版本支援)執行不區分大小寫的搜尋。例如:

find . -iname '*.txt' -print  | xargs  mv '{}'  /txts;

4、按日期查詢檔案

幾個月前,有人給你發了一張 JPEG 圖片,你接收後就儲存了起來,但現在記不清放哪了。怎樣才能找到這張圖片呢?

使用 find 命令的 -mtime 謂詞來檢查檔案的最後修改日期。例如:

find . -name '*.jpg' -mtime +90 -print

-mtime 謂詞接受一個引數,用於指定要搜尋的時間段。90 代表 90天。在數字前使用加號(+90)表明要搜尋的檔案是在 90 天前修改的。使用減號(-90)表明檔案是在 90 天以內修改的。如果既沒減號,也沒加號,則表明正好就是 90 天。

find 還可以使用邏輯運算子 AND、OR、NOT,如果知道檔案修改時間至少在一週(7 天)前,但不超過 14 天,那麼就可以像下面這樣將兩個謂詞結合起來。如下所示:

find . -mtime +7 -a -mtime -14 -print

5、按型別查詢檔案

你正在查詢名稱中帶有單詞“java”的目錄。先嚐試了以下命令。

find . -name '*java*' -print

找到的檔案太多了,其中還包括檔案系統中所有的 Java 原始碼檔案。使用 -type 謂詞只選擇目錄。如下:

find . -type d -name '*java*' -print

同樣,我們可以使用 -type f指定指查詢檔案。

我們將 -type d 放在前面,然後是 -name 'java'。兩者的順序並不影響最終結果,但將 -type d 放在謂詞列表的最前面能略微提高搜尋效率:對於碰到的每個檔案,先測試其是否為目錄,如果是,才測試名稱是否符合模式。目錄的數量比檔案要少一些。因此,這種測試順序使得大部分檔案不用再進一步比較名稱了。

6、按內容查詢檔案

你之前寫了一份重要的信件,並將其儲存為以 .txt 為副檔名的文字檔案,但現在想不起檔名的其餘部分了。除此之外,唯一記得的就是信件內容中用到過單詞“portend”。那麼該如何查詢已知部分內容的檔案呢?

如果檔案就在當前目錄下,可以使用簡單的 grep 命令。

grep -i portend *.txt

如果還沒找到,我們換用一個更完備的解決方案:find 命令。使用其 -exec 選項對滿足謂詞的檔案執行命令。你可以按下列方式使用grep 或其他實用工具:

find . -name '*.txt' -exec grep -Hi portend '{}' \;

或者也可以使用xargs

find . -name '*.txt' | xargs grep -Hi portend

Shell 文字解析awk

1、保留部分輸出

你需要用某種方法保留部分輸出,丟棄其餘輸出。比如我們日常線上日誌,我們可能會輸出很多屬性,但是真正能用來解決實際問題的,大多是我們輸出的文字資訊。以下程式碼會打印出所有輸入行的第一個欄位:

awk '{print $1}' myinput.file

欄位之間以空白字元分隔。實用工具 awk 從命令列上指定的檔案中讀取資料,如果沒有指定檔案,則從標準輸入讀取。$1代表每行以空格分割後的第一列。

除了上面的寫法,我們還可以通過管道傳入:

cat myinput.file | awk '{print $1}'

awk 的用法多變。最簡單的用法就是從輸入中打印出所選的一個或多個欄位。欄位之間以空白字元分隔(也可以用 -F 選項指定分隔字元),編號從 1 開始。欄位 $0 代表整個輸入行

2、保留部分輸入行

你只想保留部分輸入行,例如第一個和最後一個欄位。舉例來說,你希望 ls 只列出檔名和許可權,不需要 ls -l 所提供的其他資訊。可惜的是,ls 並沒有相應的選項能夠按照這種方式限制輸出。可以通過管道將 ls 的輸出傳給 awk,並從中挑選出你需要的欄位,如下:

$ ls -l | awk '{print $1, $NF}'
total 151130
-rw-r--r-- add.1
drwxr-xr-x art
drwxr-xr-x bin
-rw-r--r-- BuddyIcon.png
drwxr-xr-x CDs
drwxr-xr-x downloads
drwxr-sr-x eclipse
...
$

如果我們用ls -l 命令的輸出。其形式如下所示:

drwxr-xr-x 2 username group 176 2026-10-28 20:09 bin

對於 awk 而言,解析這種輸出易如反掌(在 awk 中,預設的欄位分隔符為空白字元)。

在輸出檔名時,我們用了點小技巧。在 awk 中,各種欄位是用美元符號和欄位編號來引用的(如 $1、$2、$3),而且 awk 還有一個內建變數 NF,其中儲存著當前行中的欄位總數,$NF 總是引用最後一個欄位。(例如,ls 的輸出行共有 8 個欄位,因此變數 NF 的值就是 8,$NF 指向的就是輸入行中的第 8 個欄位,在這個例子中就是檔名)。

注意:讀取 awk 變數時不需要使用 $(這一點和 bash 變數不同)。NF 本身就是一個有效的變數引用。在其之前加上 $ 就將其含義從“當前行的欄位總數”改成了“當前行的最後一個欄位”。

3、顛倒每行的單詞

如果想按照逆序輸出輸入行中的單詞。通過下列指令碼:

$ awk '{
> for (i=NF; i>0; i--) {
> printf "%s ", $i;
> }> printf "\n"
> }' <filename>

字元 > 不用你輸入,shell 會輸出該字元來提醒你還沒有敲完命令(shell 在查詢能配對的單引號)。由於 awk 程式位於單引號中,因此 bash shell 允許我們輸入多行程式碼,同時使用 > 作為輔助提示符,直到我們給出與先前匹配的結束單引號。考慮到可讀性,我們在程式中加入了空白字元,不過也完全可以寫成一行。

$ awk '{for (i=NF; i>0; i--) {printf "%s ", $i;} printf "\n"}' <filename>

awk 語言的 for 迴圈語法和 C 語言中的非常相似。我們用 for 迴圈從最後一個欄位開始倒著處理到第一個欄位,同時輸出每個欄位的內容。

4、彙總數字列表

如果你需要彙總數字列表,其中有些數字並未出現在行中。用 awk 先過濾出待彙總的欄位,然後再做彙總。這裡我們要對 ls -l 命令輸出的檔案大小進行彙總。如下:

ls -l | awk '{sum += $5}; END {print sum}'

我們要彙總 ls -l 輸出的第 5 個欄位。ls -l 的輸出如下所示:

-rw-r--r--. 1 root root  37 12月  23 21:44 2.text                                           
-rwxr--r--. 1 root root 110 12月  10 02:20 ifTest.sh

各個欄位分別為:許可權、連結、所有者、所屬組、大小(以位元組為單位)、最後一次修改日期、最後一次修改時間,以及檔名。我們只對檔案大小感興趣,因此在 awk 程式中用 $5 來引用該欄位。我們在花括號({})裡放置了兩段 awk 程式碼,注意,awk 程式中可以有多個程式碼段(或程式碼塊)。前有關鍵詞 END 的程式碼塊僅在程式其他部分完成後執行一次。

本文由傳智教育博學谷教研團隊釋出。

如果本文對您有幫助,歡迎關注點贊;如果您有任何建議也可留言評論私信,您的支援是我堅持創作的動力。

轉載請註明出處!