Contents

删除文件中列出的文件

1. 概述

当我们在 Linux 命令行下工作时,我们经常需要对文件进行操作。删除文件是一种常见的操作。 我们可能会面临不同的文件删除需求——例如,删除超过给定时间的 文件、递归删除具有特定扩展名的 文件、一次删除多个文件 等等。

这一次,我们将讨论如何删除另一个文件中列出的文件。

2. 问题介绍

2.1. 问题的例子

一个例子可以更容易地解释这个问题。 首先,我们准备了一个目录 delTest和它下面的一些文件:

$ tree /tmp/delTest/
/tmp/delTest/
├── jpg_files
│   ├── olympic tokyo 001.jpg
│   ├── olympic tokyo 002.jpg
│   └── olympic tokyo 003.jpg
├── pdf_files
│   ├── bar.pdf
│   └── foo.pdf
├── toDelete.txt
└── txt_files
    ├── bar.txt
    └── foo.txt
3 directories, 8 files

我们使用tree 命令更清晰地输出目录结构。请注意,jpg_files目录下的文件名包含空格。

假设我们在toDelete.txt文件中定义了要删除的文件:

$ cat toDelete.txt 
/tmp/delTest/pdf_files/foo.pdf
/tmp/delTest/txt_files
/tmp/delTest/jpg_files/olympic tokyo 001.jpg

我们的目标是删除toDelete.txt文件中列出的文件。

2.2. 解决问题的思路

由于toDelete.txt文件中已经定义了我们要删除的文件和目录,所以我们有两个思路来解决这个问题:

  • 读取toDelete.txt文件中的每一行,并对每一行执行rm命令以删除该文件。
  • 读取toDelete.txt文件的内容并将这些行转换为一堆rm命令。最后,将结果通过管道传递给sh命令以执行它们。

我们将介绍三种方法来涵盖这两种想法:

  • 使用纯 Bash
  • 使用/xargs 命令
  • 使用 sed 命令
  • 使用 awk  命令

现在,让我们看看他们的行动。

3. 使用纯 Bash

今天,Bash 已成为大多数现代 Linux 发行版的默认 shell。所以, 如果我们用纯 Bash 解决了一个问题,也就是说,我们的解决方案不依赖任何额外的依赖

3.1. 处理目录

如示例所示,问题看起来很简单。但是,该问题可能有一些变体。现在,让我们仔细看看它们。

toDelete.txt文件中列出的 文件可能包含目录。例如,/tmp/delTest/txt_files是一个目录。

根据要求,我们可能希望跳过删除目录或递归删除目录。

rm命令提供了一个*-r 选项。此选项允许我们以递归方式删除目录。此外,**如果我们对常规文件执行rm -r*,它也会删除该文件**。

因此,如果需求是删除 toDelete.txt文件中的文件和目录,我们可以对文件中的行执行 rm -r。否则,我们调用 不带 -r选项的rm来跳过删除目录

在本教程中,假设要求是删除目录和文件。也就是说,我们将使用 带有*-r选项的**rm*作为我们的文件删除命令。

3.2. 引用文件名

大多数 Linux 文件系统都接受文件名中的空格。例如,文件*/tmp/delTest/jpg_files/olympic tokyo 001.jpg*包含多个空格。

当我们通过文件名操作文件时,引用文件名是一个好习惯。或者我们可能会看到意想不到的结果。

但是,值得一提的是,引用文件名将禁用 glob 扩展,无论我们使用单引号还是双引号。

我们还假设支持 glob 不是这个问题的要求。这是因为如果toDelete.txt文件中有一些错误,在现实世界中可能会很危险。例如,如果我们在/path/*/*上运行rm -r ,它会删除很多文件。

因此,我们将始终在所有解决方案中引用文件名。

现在,让我们编写一个简单的 shell 脚本来读取toDelete.txt文件并删除这些文件。

3.3. 简单的 Bash 脚本

首先,让我们看一下脚本:

$ cat delFromFile.sh 
#!/bin/bash
TO_BE_DEL="$1"
IFS=""
while read -r file ; do
    rm -r "$file"
done < "$TO_BE_DEL"

剧本不难理解。它只包含一个while循环。让我们快速通过它。

首先,我们将IFS 变量设置为空,以便toDelete.txt中的每一行在我们read 时都成为一条记录。

while循环中,我们 对toDelete.txt文件中定义的每个目录或文件 执行rm -r命令。

接下来,让我们测试一下脚本,看看它是否像我们预期的那样工作:

$ ./delFromFile.sh /tmp/delTest/toDelete.txt 
$ tree /tmp/delTest
/tmp/delTest
├── jpg_files
│   ├── olympic tokyo 002.jpg
│   └── olympic tokyo 003.jpg
├── pdf_files
│   └── bar.pdf
└── toDelete.txt
2 directories, 4 files

执行完脚本后,我们再次调用tree命令来验证结果。我们看到我们已经成功删除了toDelete.txt 中列出的目录和文件。

因此,我们解决了这个问题。

4. 使用xargs命令

** xargs命令从stdin读取输入并将其转换为参数以提供给其他命令 。**

现在,让我们看一下用于删除toDelete.txt文件中列出的文件的单行xargs命令:

xargs -I{} rm -r "{}" < /tmp/delTest/toDelete.txt

由于 xargs命令仅从stdin读取,我们将输入文件toDelete.txt重定向到stdin。此外,我们定义了一个占位符“{}”,以便我们可以在 rm命令中引用文件名。

接下来,让我们恢复 /tmp/delTest目录下的文件并测试这个命令:

$ xargs -I{} rm -r "{}" </tmp/delTest/toDelete.txt
$ tree /tmp/delTest
/tmp/delTest/
├── jpg_files
│   ├── olympic tokyo 002.jpg
│   └── olympic tokyo 003.jpg
├── pdf_files
│   └── bar.pdf
└── toDelete.txt
2 directories, 4 files

该命令按我们的预期工作。

5. 使用sed命令

到目前为止,我们已经了解了如何使用纯 Bash 和xargs来解决问题。这两种方法都从 toDelete.txt文件中获取每一行并提供rm -r命令。

我们可以按照另一种思路,将文件内容转换成多个 rm命令。

sed命令可以使用紧凑的 单线轻松处理此类任务:

$ sed 's/.*/rm -r "\0"/' /tmp/delTest/toDelete.txt
rm -r "/tmp/delTest/pdf_files/foo.pdf"
rm -r "/tmp/delTest/txt_files"
rm -r "/tmp/delTest/jpg_files/olympic tokyo 001.jpg"

如上面的输出所示,  sed 命令行通过替换构建了我们需要的rm命令。此外,文件名也被正确引用。

虽然sed命令本身不会执行实际删除,但我们有机会检查它产生的rm命令。因此,它可以帮助我们发现错误。

如果命令看起来不错,我们可以将 sed 命令行的结果通过管道传输到 sh以删除文件。

接下来,让我们恢复文件并测试我们的 sed 命令行:

$ sed 's/.*/rm -r "\0"/' /tmp/delTest/toDelete.txt | sh
$ tree /tmp/delTest
/tmp/delTest/
├── jpg_files
│   ├── olympic tokyo 002.jpg
│   └── olympic tokyo 003.jpg
├── pdf_files
│   └── bar.pdf
└── toDelete.txt
2 directories, 4 files

6. 使用 awk命令

toDelete.txt文件构建rm命令对于awk来说是小菜一碟。同样,我们也可以使用awk的替换函数来完成这项工作。

但是,在这里,我们展示了另一种方法:

$ awk -v q='"' '$0 = "rm -r " q $0 q' /tmp/delTest/toDelete.txt
rm -r "/tmp/delTest/pdf_files/foo.pdf"
rm -r "/tmp/delTest/txt_files"
rm -r "/tmp/delTest/jpg_files/olympic tokyo 001.jpg"

让我们快速了解一下简短的 awk命令是如何工作的。

为了避免转义引号字符并使代码更易于阅读,我们声明了一个awk变量 q来存储双引号字符。

我们知道,在我们将“ rm -r ”和引号连接到原始输入行之后,结果一定不能是空字符串。

** awk将评估任何非空和非零字符串为True。此外,语句*“True”将触发默认操作:打印当前处理行。*

因此,awk打印生成的rm命令。

与 sed解决方案相同,如果我们想要进行实际删除,只需将 awk输出通过管道传输到 sh

$ awk -v q='"' '$0="rm -r " q $0 q' /tmp/delTest/toDelete.txt | sh
$ tree /tmp/delTest
/tmp/delTest/
├── jpg_files
│   ├── olympic tokyo 002.jpg
│   └── olympic tokyo 003.jpg
├── pdf_files
│   └── bar.pdf
└── toDelete.txt
2 directories, 4 files