公众号旧文

Linux 中如何从 N 行文件里删除指定的 M 行

作者:胖子阅读 —

如果是在 Windows 上,从 N 行文件里删除指定的 M 行,你是不是需要复制第一行,然后去文件中查找,然后再删除整行,然后再继续删除第二行,循环往复 M 次?


直到我遇见了 Linux

在处理文本文件时,经常会遇到这样一个问题:

有一个包含N 行数据的原文件:

original.txt

还有一个包含M 行待删除内容的名单文件:

delete.txt

目标是:

从 original.txt 中删除所有出现在 delete.txt 里的整行内容,并生成一个新的文件。

一个非常简洁、可靠的做法是使用 grep。

一、先查看哪些行会被删除

执行:

grep -xFf delete.txt original.txt

这条命令不会修改任何文件,它只是从 original.txt 中找出所有命中删除名单的行。

假设:

original.txt

内容是:

apple
banana
orange
grape
watermelon
pear

而:

delete.txt

内容是:

banana
grape

执行:

grep -xFf delete.txt original.txt

输出:

banana
grape

这说明:

original.txt
        ↓
逐行检查
        ↓
是否存在于 delete.txt
        ↓
存在 → 输出
不存在 → 忽略

如果想知道一共命中了多少行,可以接 wc -l:

grep -xFf delete.txt original.txt | wc -l

输出:

2

也就是说,删除名单中的 2 行确实存在于原文件中。


二、真正删除这些行

核心命令是:

grep -vxFf delete.txt original.txt > new.txt

执行后:

new.txt

内容会变成:

apple
orange
watermelon
pear

原文件有 6 行,删除 2 行以后:

6 - 2 = 4

新文件正好剩下 4 行。


三、-xFf 到底是什么意思

这条命令的关键在于:

grep -xFf delete.txt original.txt

可以拆成:

grep
-x
-F
-f delete.txt
original.txt

-f delete.txt

-f 表示:

从一个文件中读取匹配规则。

也就是说:

-f delete.txt

告诉 grep:

不要只匹配一个字符串,而是把 delete.txt 中的每一行都当成一个待匹配内容。

例如:

delete.txt

里面有:

banana
grape
pear

实际上等价于告诉 grep:

匹配 banana
或者 grape
或者 pear

因此删除名单可以有几十行、几百行甚至更多,不需要手工写一长串条件。


-F:按普通字符串匹配

grep 默认把匹配内容当作正则表达式。

例如:

.
*
[
]
^
$

这些字符在正则表达式中都有特殊含义。

但很多时候,我们只是想进行:

字符串完全一致比较。

这时使用:

-F

也就是 fixed strings。

它告诉 grep:

不要解析正则表达式,把每一行原封不动地当成普通字符串。

例如删除名单中有:

file.1

使用 -F 后,. 就只是一个普通的点,而不会代表“任意字符”。

因此对于“名单匹配”这种场景,通常使用 -F 更安全。


-x:必须整行完全匹配

-x 表示:

只有整行内容完全一样,才算匹配。

例如原文件中有:

apple
apple pie
pineapple

删除名单中只有:

apple

如果使用:

grep -xFf delete.txt original.txt

只会匹配:

apple

不会匹配:

apple pie
pineapple

这对于“删除指定行”非常重要。

因为通常我们想表达的是:

删除这一整行

而不是:

删除所有包含这个字符串的行

四、-v 为什么能把“查找”变成“删除”

前面的命令:

grep -xFf delete.txt original.txt

含义是:

输出所有匹配删除名单的行。

加入:

-v

以后:

grep -vxFf delete.txt original.txt

意思变成:

输出所有“不匹配删除名单”的行。

-v 就是 invert match,也就是反选。

因此:

没有 -v

逻辑是:

匹配 → 保留
不匹配 → 丢弃

而加上:

-v

以后变成:

匹配 → 丢弃
不匹配 → 保留

这正好实现了“删除指定行”。


五、整个过程的逻辑

可以把它理解为一个集合操作。

假设原文件:

original.txt

包含 N 行。

删除文件:

delete.txt

包含 M 行。

我们真正想得到的是:

new = original - delete

也就是:

原集合减去删除集合。

grep 的处理过程相当于:

读取 original.txt 第一行
        ↓
检查这一行是否存在于 delete.txt
        ↓
      ┌─────────┐
      │         │
     是         否
      │         │
      ↓         ↓
    丢弃       输出

然后继续处理下一行,直到原文件结束。

所以:

grep -vxFf delete.txt original.txt > new.txt

本质上就是:

遍历 N 行
+
使用 M 行作为排除名单
+
输出剩余内容

六、为什么先输出到 new.txt

可能有人会想直接写:

grep -vxFf delete.txt original.txt > original.txt

这是错误的。

原因不是 grep 本身,而是 Shell 的输出重定向机制。

当 Shell 看到:

> original.txt

时,会在启动 grep 之前先把:

original.txt

清空。

因此实际顺序可能变成:

Shell 清空 original.txt
        ↓
grep 开始读取 original.txt
        ↓
发现文件已经是空的
        ↓
最终什么都没有

所以千万不要让输入文件和 > 后面的输出文件是同一个文件。

正确做法是:

grep -vxFf delete.txt original.txt > new.txt

确认结果正确以后,再替换原文件:

mv new.txt original.txt

完整流程:

original.txt
      ↓
grep 读取
      ↓
过滤删除名单
      ↓
new.txt
      ↓
检查无误
      ↓
mv
      ↓
original.txt

七、更加安全的实际操作流程

如果原文件比较重要,可以先备份:

cp original.txt original.txt.bak

然后查看命中内容:

grep -xFf delete.txt original.txt

统计命中数量:

grep -xFf delete.txt original.txt | wc -l

执行删除:

grep -vxFf delete.txt original.txt > new.txt

查看新文件行数:

wc -l original.txt new.txt

最后确认删除名单已经没有任何命中:

grep -xFf delete.txt new.txt

如果没有输出,就表示指定内容已经全部被过滤掉。

确认无误以后:

mv new.txt original.txt

八、一个完整示例

原文件:

original.txt

有 8 行:

A
B
C
D
E
F
G
H

删除名单:

delete.txt

有 3 行:

B
E
G

先检查:

grep -xFf delete.txt original.txt

得到:

B
E
G

统计:

grep -xFf delete.txt original.txt | wc -l

得到:

3

执行删除:

grep -vxFf delete.txt original.txt > new.txt

新文件:

A
C
D
F
H

检查行数:

wc -l original.txt new.txt

可能得到:

8 original.txt
5 new.txt

正好满足:

8 - 3 = 5

九、两条命令可以这样记

如果只想记住最核心的内容,其实就是两条。

查看哪些行命中删除名单

grep -xFf delete.txt original.txt

可以理解为:

精确找出 delete.txt 中存在于 original.txt 的行

删除这些行

grep -vxFf delete.txt original.txt > new.txt

可以理解为:

保留所有不在 delete.txt 中的行

两者唯一关键的区别就是:

-v

没有 -v:

找出黑名单。

有 -v:

排除黑名单。


十、一句话理解

从一个 N 行文件中删除指定的 M 行,本质上不一定需要真的去“编辑并删除”原文件。

更简单的思路是:

重新生成一份文件,只把不在删除名单里的行写进去。

也就是:

原文件 N 行
        ↓
删除名单 M 行
        ↓
逐行比较
        ↓
命中名单 → 不输出
未命中 → 输出
        ↓
得到过滤后的新文件

而:

grep -vxFf delete.txt original.txt > new.txt

就是这个逻辑非常简洁的 Linux 实现。