Journey of a noob

Learn From Noobs

[Unix/Linux] 刪除在另一個文件中存在的行

Posted at # Unix/Linux

[Unix/Linux] 刪除在另一個文件中存在的行

有時,我們需要在一個文件中刪除在另一個文件中存在的行,這通常發生在當我們需要從配置文件中刪除一些配置的時候,如果只有幾行需要刪除,這當然很容易,但是當我們要在多台主機上刪除許多行時,這將變得乏味且容易出錯, 我們應該使用一些自動的方法,而不是手動地把一行行刪除。(當然,我們仍然應該核對最後的結果是否正確。)

我們將使用一個簡單的示例來說明如何刪除這些行,假設我們正在組織一個派對,我們有兩個文件:all.txt 包含我們所有的朋友,而 remove.txt 包含不參加派對的朋友。

all.txt

John
Tom
Tony
Alex
Michael
Kalvin

remove.txt

Tony
Alex
Chris

留意雖然 “Chris” 存在於 remove.txt 內, 但卻不存於 all.txt 之中,在現實情況中,被要求刪除一行不存在的配置也是件相當平常的事。


如果文件中行的排序不能改變

1. grep -Fvxf remove.txt all.txt

我們可以使用 grep 指令過濾掉不需要的行。

noob@learnfromnoobs:~$ grep -Fvxf remove.txt all.txt
John
Tom
Michael
Kalvin

上述指令的一些注意事項:

  1. -Fgrep 將模式 PATTERN(remove.txt 中的內容)視為一個固定的字串(fixed-string)而不是正則表達式 (regular expression),在我們的示例中,它沒有任何作用,但當我們修改配置文件時,我們總會遇到一些特殊字符,我們需要確保它們不會被視為正則表達式,否則,這指令可能會刪除比預期更多或更少的行。
  2. -v 只會選擇不匹配的行(all.txt 中存在但 remove.txt 中不存在的行)。
  3. -x 只選擇能匹配完整一行的匹配。
  4. -f 使我們可以從文件中獲取模式 PATTERN。

如想知道更多,請參閱 man grep

2. awk ‘NR==FNR{array[0];next} !(0 in array)’ remove.txt all.txt

當處理這類問題時,awk 會是我們的好幫手。

noob@learnfromnoobs:~$ awk 'NR==FNR{array[$0];next} !($0 in array)' remove.txt all.txt
John
Tom
Michael
Kalvin

我們可以把指令分拆成幾個部分:

  1. NR==FNR 是確定我們是否正在讀取第一個文件(在本例中的 remove.txt)的常用方法。
  2. NR==FNR{array[$0];next} 表示如果當前的行是來自第一個文件,則將該行保存到陣列(array)中。
  3. 從第二個文件(all.txt)中讀取行時,我們使用 !($0 in array) 來檢查當前的行是否在我們之前創建的陣列中(即 remove.txt 中的行),注意指令中缺少了 awk 的默認操作 {print} ,這是一種常見的做法,因此,我們在此處使用的實際指令是 !($0 in array){print},意味著如果當前的行不在陣列中(再提一次,即 remove.txt 中的行),我們就要把該行顯示出來。

3. diff —new-line-format="" —old-line-format=“%L” —unchanged-line-format="" all.txt remove.txt

我們也可以使用 diff 得到相同的結果,不過我們需要設定好 diff 輸出的格式。

noob@learnfromnoobs:~$ diff --new-line-format="" --unchanged-line-format="" --old-line-format="%L" all.txt remove.txt
John
Tom
Michael
Kalvin

如果你對理解這指令感到困難,我們可以將它分為幾部分。

  1. 讓我們查看一下 diff 原來的結果,我們可以看到它顯示了刪除/添加了哪些行,但它卻不會顯示 all.txtremove.txt 中都存在的那些行(“Tony” 和 “Alex”)。
noob@learnfromnoobs:~$ diff all.txt remove.txt
1,2d0
< John
< Tom
5,6c3
< Michael
< Kalvin
---
> Chris
  1. 現在,我們在指令中添加參數 --new-line-format=“” 來刪除添加了的行(“Chris”)。
noob@learnfromnoobs:~$ diff --new-line-format="" all.txt remove.txt
John
Tom
Tony
Alex
Michael
Kalvin
  1. 你可能也留意到,現在我們要刪除的行再次出現了(“Tony” 和 “Alex”), 在上一步中,我們已經處理了新行的格式,我們亦有責任處理那些沒有更改的行以及舊行的格式,我們可以通過設置 --unchanged-line-format="" 來刪除沒有更改的行;對於舊行,我們則可以設置 --old-line-format="%L" 來把它們原原本本的顯示出來。
noob@learnfromnoobs:~$ diff --new-line-format="" --unchanged-line-format="" --old-line-format="%L" all.txt remove.txt
John
Tom
Michael
Kalvin

最後,diff 只輸出了沒有更改的行(即 all.txt 特有的行)。

如想知道更多,請參閱 GNU Diffutils - Line Formats


如果文件中行的排序可以改變

4. comm -23 all.txt remove.txt

如果文件中行的排序對你來說並不重要,你可以考慮使用 comm 指令來刪除不需要的行,若要使用此方法,我們必須確保文件已被排序。

noob@learnfromnoobs:~$ sort all.txt > all_sorted.txt
noob@learnfromnoobs:~$ sort remove.txt > remove_sorted.txt
noob@learnfromnoobs:~$ comm -23 all_sorted.txt remove_sorted.txt
John
Kalvin
Michael
Tom

在執行 comm 指令時,我們還可以利用 Process Substitution(進程替代)把文件內容排序。

noob@learnfromnoobs:~$ comm -23 <(sort all.txt) <(sort remove.txt)
John
Kalvin
Michael
Tom

5. join -v 1 all.txt remove.txt

join -v FILENUM all.txt remove.txt 可用作刪除兩個文件中的相同的行,這指令會顯示 FILENUM 中的所有行,但並不顯示另一個文件中的內容,在我們的指令中,我們把 FILENUM 設置為 1,所以它會顯示第一個文件(本例中的 all.txt)中的所有行,但不顯示另一個文件(remove.txt)中的內容。

注意使用此方法時,我們也需要確保對輸入文件已經排序。

noob@learnfromnoobs:~$ join -v 1 all_sorted.txt remove_sorted.txt
John
Kalvin
Michael
Tom

同樣地,當使用 join 指令時,我們也可以使用 Process Substitution 把文件排序。

noob@learnfromnoobs:~$ join -v 1 <(sort all.txt) <(sort remove.txt)
John
Kalvin
Michael
Tom

總結

在本文中,我們一共討論了五種不同的方法令我們可以在一個文件中刪除在另一個文件中存在的行:

  1. grep -Fvxf remove.txt all.txt
  2. awk 'NR==FNR{array[$0];next} !($0 in array)' remove.txt all.txt
  3. diff --new-line-format="" --old-line-format="%L" --unchanged-line-format="" all.txt remove.txt
  4. comm -23 all.txt remove.txt
  5. join -v 1 all.txt remove.txt

無論你最後採用了哪個方法,我都希望你享受閱讀這篇文章。

記得要不斷學習,have fun!