Contents

查找和删除文件和目录

1. 概述

在 Linux 命令行下,我们可以使用find 命令来获取文件或目录的列表。通常,我们希望对找到的文件进行一些操作,例如findtar文件

在本教程中,我们将看看如何删除我们找到的文件或目录。

2. 问题介绍

有几种方法可以删除find命令找到的文件和目录。这不是一个难题。或许我们心中已经有了一些解决方案。

但是,如果我们没有正确使用某些解决方案,它们可能会很危险。此外,某些解决方案在性能方面可能效果不佳。

在本教程的其余部分,我们将仔细研究使用find命令的常见缺陷,并解释为什么它很危险。

此外,我们还将讨论性能。

首先,让我们创建一个目录结构作为示例:

$ tree -a test
test
├── kotlin
│   ├── ktApp1
│   │   └── .git
│   │       └── whatever.txt
│   └── ktApp2
│       └── .git
│           └── whatever.txt
└── python
    ├── pyApp1
    │   └── .git
    │       └── whatever.txt
    └── pyApp2
        └── .git
            └── whatever.txt
10 directories, 4 files

如上面的tree 输出所示,我们创建了一个 包含一些子目录和文件的test目录。

我们将在test目录上尝试两次删除:

  • 文件删除:删除所有 whatever.txt文件
  • 目录删除:全部删除*。**git*目录及其下的文件

让我们看一下find命令来查找我们的目标目录和文件。

首先,让我们找到所有whatever.txt文件:

$ find test -name 'whatever.txt'
test/python/pyApp2/.git/whatever.txt
test/python/pyApp1/.git/whatever.txt
test/kotlin/ktApp2/.git/whatever.txt
test/kotlin/ktApp1/.git/whatever.txt

同样,我们也可以找到 .git目录:

$ find test -type d -name '.git'
test/python/pyApp2/.git
test/python/pyApp1/.git
test/kotlin/ktApp2/.git
test/kotlin/ktApp1/.git

在本教程中,我们将介绍三种删除目标文件和目录的方法:

  • 使用 find命令的 -delete操作
  • 使用 find -exec
  • 使用 查找 | xargs rm

到目前为止,我们已经了解了如何使用find命令找到要删除的文件或目录。此外,我们知道我们可以将 Linux 命令与管道 连接起来,让不同的命令协同解决我们的问题。

我们中的许多人可能认为解决这个问题最直接的方法是将查找结果通过管道传递给rm命令。有点令人惊讶的是,它不在上面的项目符号列表中。

因此,在我们查看问题的真正解决方案之前,让我们了解为什么我们不能将find的结果通过管道传递给rm

3. 为什么* “find … | rm” *不行吗?

在回答这个问题之前,我们需要了解管道的作用。首先,让我们看一个例子:

$ ls -1 / | grep '^m'
media/
mnt/

在上面的简单示例中,我们将ls命令的结果通过管道传递给grep并找出名称以“m”开头的根目录。

简单来说,这里管道将ls的标准输出(Stdout)转换为grep命令的标准输入(Stdin)。

此命令有效,因为grep命令接受来自 Stdin 的读取。我们可以通过管道将标准输出传递给支持从标准输入读取的更多命令,例如:

$ ls -1 / | grep '^m' | sed 's/^m/OK_m/'
OK_media/
OK_mnt/

我们可以在现实世界中经常看到这种“命令链”。

但是,并非所有 Linux 命令都支持从 Stdin 读取。典型示例是执行文件处理的命令,例如 cpmv和 rm。这些命令忽略 Stdin

例如,当我们执行命令“ rm file ”时,rm接受命令行参数file,它表示一个文件。它根本不会读取标准输入:

$ echo "file" | rm
rm: missing operand

因此,“find …. | rm”也行不通。

但是,有时我们想以某种方式将一个命令的标准输出转换为另一个命令的参数。这就是xargs派上用场的地方。我们将在后面的部分中看到它的作用。

现在,让我们探讨一下“查找和删除”问题的解决方案。

4. 使用find命令和 -delete操作

** find命令提供了一个 -delete操作来删除文件。**接下来,让我们使用此操作删除目标文件和目录。

4.1.删除目标文件和目录

我们可以 通过在find命令中添加*-delete选项来 删除所有whatever.txt*文件:

$ find test -name 'whatever.txt' -delete
$ tree -a test
test
├── kotlin
│   ├── ktApp1
│   │   └── .git
│   └── ktApp2
│       └── .git
└── python
    ├── pyApp1
    │   └── .git
    └── pyApp2
        └── .git
10 directories, 0 files

很好,它有效。所有 whatever.txt文件都已被删除。

接下来,让我们恢复 test目录并尝试 递归删除*.git*目录:

$ find test -type d -name '.git' -delete
find: cannot delete ‘test/python/pyApp2/.git’: Directory not empty
find: cannot delete ‘test/python/pyApp1/.git’: Directory not empty
find: cannot delete ‘test/kotlin/ktApp2/.git’: Directory not empty
find: cannot delete ‘test/kotlin/ktApp1/.git’: Directory not empty

糟糕,这一次,我们收到了错误消息。这是因为*-delete*操作不能以递归方式删除非空目录。也就是说,它只能删除文件和空目录。

4.2. -delete用法的危险陷阱

接下来,我们来做一个有趣的测试。我们知道 Linux 命令的选项顺序通常并不重要。

例如,以下两个ls命令是相同的,即使选项的顺序不同:

ls -F -a -l --color
ls -l -a --color -F

现在,让我们通过将*-delete*选项移动到第一个位置来重新排序最后一个 find命令中的选项,看看会发生什么:

$ find test -delete -type d -name '.git'
$ ls test
ls: cannot access 'test': No such file or directory

这一次,没有错误消息。表示命令执行成功。

但是,当我们检查结果时,我们发现 test目录已经被完全删除了!让我们了解它为什么会发生。

让我们重温一下我们的find命令。我们可以调用三个选项:-delete-type d和*-name ‘.git’*。但是,我们不应该忘记 ** find也将它们视为三个表达式**。

将评估find命令中的表达式 ,返回一个布尔值,并且-delete*操作始终返回 true。*

如果 -delete操作位于第一个位置,则在其评估期间,它将删除给定目录及其中的所有内容,即我们示例中的 test目录。

但是等等——我们刚刚了解到*-delete操作不会删除非空目录。为什么所有正在测试*的东西都 被删除了?

这是因为*-delete*操作隐含了 -depth选项

-depth选项要求find命令**在目录本身之前搜索每个目录的内容。因此,如果我们将 -delete作为第一个选项,它将从每个目录树的最底部开始删除。首先,它删除一个目录下的所有文件,然后是空目录本身,直到所有内容都被删除。

当我们使用find命令时,我们应该记住,我们不应该将*-delete*操作放在第一个位置。如果我们这样做,它可能会意外删除文件。

5. 使用find -exec

当我们使用带有*-exec操作的find命令时,我们可以对其结果执行外部命令。现在,让我们执行rm*命令以这种方法删除我们的目标文件和目录:

$ find test -name 'whatever.txt' -exec rm {} \;
$ tree -a test
test
├── kotlin
│   ├── ktApp1
│   │   └── .git
│   └── ktApp2
│       └── .git
└── python
    ├── pyApp1
    │   └── .git
    └── pyApp2
        └── .git
10 directories, 0 files

很好,所有whatever.txt文件都已删除。当我们将 -exec与外部命令一起使用时,它将填充“{}”占位符中的每个找到的文件。

同样,如果我们在rm命令中添加*-r*选项 ,我们可以删除所有 .git目录。让我们恢复 test目录并试一试:

$ find test -depth -type d -name '.git' -exec rm -r '{}' \;
$ tree -a test
test
├── kotlin
│   ├── ktApp1
│   └── ktApp2
└── python
    ├── pyApp1
    └── pyApp2
6 directories, 0 files

如输出所示,所有 .git目录均已成功删除。

6. 使用find | xargs rm组合

现在,我们了解到我们可以 使用find的*-exec动作来执行rm命令 。或者,我们也可以将 find命令的结果通过管道传递给xargs*并让 xargs调用 rm命令来删除这些文件。

接下来,让我们看看如何使用这种方法删除所有 whatever.txt文件:

$ find test -name 'whatever.txt' | xargs rm
$ tree -a test
test
├── kotlin
│   ├── ktApp1
│   │   └── .git
│   └── ktApp2
│       └── .git
└── python
    ├── pyApp1
    │   └── .git
    └── pyApp2
        └── .git
10 directories, 0 files

同样,我们也可以用同样的方法删除所有的*.git目录。让我们恢复test*目录并测试它:

$ find test -type d -name '.git' | xargs rm -r
$ tree -a test
test
├── kotlin
│   ├── ktApp1
│   └── ktApp2
└── python
    ├── pyApp1
    └── pyApp2
6 directories, 0 files

如上面的输出所示,所有*.git*目录都已被删除。

我们可能会问:如果find -exec rm可以解决问题,为什么还要引入一个额外的xargs进程来做同样的事情呢? 要了解答案,让我们讨论一下他们的表现。

7. 对find -execfind | xargs 的性能进行基准测试

首先,让我们解释一下find -exec rm方法的工作原理。当我们使用这种方法时,将为find命令找到的每个文件执行一个rm进程。也就是说, 如果find命令找到一百万个文件,我们将执行rm命令一百万次。

另一方面,如果我们执行 find | xargs rmxargs会将找到的文件构建到包中,并尽可能少地通过命令运行它们**。

因此,如果我们的 find命令返回大量文件或目录,  find | xargs命令将比 find -exec命令方法快得多。

接下来,让我们对每种方法进行相同的性能测试并对其性能进行基准测试。

我们将使用每个命令删除3000个文件,并使用time 命令测量它们的执行时间。

首先,让我们使用 find -exec rm方法进行测试:

$ touch {1..3000}.txt
$ ls -l *.txt | wc -l
3000
$ time find . -name '*.txt' -exec rm '{}' \;
real	0m6.072s
user	0m3.130s
sys	0m2.932s

在这台机器上,删除所有文件大约需要六秒钟。

接下来, 轮到xargs了。让我们看看它是否可以更快地进行相同的测试:

$ touch {1..3000}.txt
$ ls -l *.txt | wc -l
3000
$ time find . -name '*.txt' | xargs rm
real	0m0.053s
user	0m0.029s
sys	0m0.029s

这一次, 删除文件只用了0.05秒。与 find -exec rm方法相比, 在此测试中使用xargs120倍!哇!

因此,如果find返回大量文件,我们应该考虑将结果通过管道传递给xargs命令