在 Linux 中实现 deltree 命令

作者:Graydon L. Ekdahl,

是否曾经需要从文件空间中删除一个大型软件包,却发现它分散在一个目录树中,包含超过一百个文件和十分之一的子目录?rm -rf 命令可以很好地清除一切。然而,为了更多地了解如何遍历 Linux 目录树,让我们看看在 Linux 中实现一个自制的(类似 DOS 的)deltree 命令并不困难,如果您拥有该实用程序并在自己的文件空间中使用它,它将使删除未使用的或不需要的软件包变得更容易。

Linux C 库资源:文件和目录

要选择 C 库资源来完成此任务,我们首先确定 UNIX 中通常可用的资源,然后确定 Linux 实现了哪些资源。头文件 ftw.h 中包含的两个 UNIX 函数可以遍历目录树

int ftw ( const char * path,
        int (*funcptr )
        ( const char *, const struct stat*, int ),
        int depth )

int nftw (const char * path,
        int (*funcptr)
        (const char *, const struct stat*, int, struct FTW*),
        int depth,
        int flag )
Linux 没有实现第二个函数,因此我们转向第一个;ftw 从上到下遍历目录树。对于每个目录条目,ftw 调用 funcptr 指向的函数,其中包含条目的名称、指向包含 inode 信息的 stat 结构的指针以及一个标志,该标志设置为传达有关相关目录条目的信息
  • FTW_F:一个文件

  • FTW_D:一个目录

  • FTW_DNR:一个不可读的目录

  • FTW_NS:stat 失败且 inode 信息不可用。

ftw 向调用者返回什么?如果它成功完成树的遍历,则返回 0。否则,它返回 -1 并适当设置全局错误标志 errno。使用 ftw,创建一个旨在删除目录项的函数并将指向该函数的指针传递给 ftw 是一件简单的事情(参见列表 1

DelEntry() 中,C 库函数

int remove ( const char * path )

在 stdio.h 中执行实际工作。如果成功,此函数返回 0,如果不成功,则返回 -1,并根据需要设置全局变量 errno 以处理许多不同的错误情况,Linux 手册页对此进行了详细解释。

但是,有一个问题。在 UNIX 中,remove 通常可以用于删除文件或空目录。在 Linux 中,remove 仅处理文件,因此会清空目录树,但会保留树本身。两个 UNIX C 库函数可以提供解决方案

int rmdirp ( char * d, char *d1 )
int rmdir ( const char * path )

Linux 没有实现第一个 UNIX 函数 rmdirp,因此我们专注于第二个。函数 rmdir 仅删除空目录,成功时返回 0,否则返回 -1 并设置 errno。为了完成我们的任务,我们必须遍历树两次:一次从上到下到目录底部,边走边删除文件,第二次从底部返回顶部,以相反的顺序删除空目录。实现此结果的完美工具是一个容器类:指向目录路径名称的指针堆栈。

StrStack:指向 char 数组的指针堆栈

当 ftw 调用 DelEntry 时,它提供一个标志,指示它是否找到文件、目录或无法处理,我们可以使用此标志在 ftw 遍历树时用路径名填充 StrStack 在 DelEntry 内部。问题是将堆栈放在哪里。头文件 ftw.h 指定了 ftw 函数和 funcptr 指向的函数的签名,并且两个签名都不包含堆栈,因此我们不能通过引用将堆栈作为参数传递给 DelEntry。最简单的解决方案是在实现文件 funcs.cpp 中创建 StrStack 作为外部变量,该文件保存 main 的函数定义。作为外部变量,DelEntry 和 DelDirectories 都可以同样访问 StrStack,前提是它在这些函数的实现文件中定义。

StrStack 的几个方面需要解释。StrStack 与普通堆栈的不同之处在于,每个节点都包含指向两个不同动态分配的结构的指针:指向下一个节点的指针和指向不同长度的字符的指针。创建节点需要两次分配,销毁节点需要两次单独的释放。通过使 StrStack 负责两次分配,代码更可靠、更健壮且没有内存泄漏。此外,如果调用者负责分配和释放包含字符数组的内存,那么外部代码可能会从 StrStack 下拉出数据,留下悬空指针。

在某些地方,隐式递归完成了分配和释放,并且乍一看可能不明显该过程是如何工作的。让我们检查 列表 2 中显示的 StrStack 的复制构造函数。

类复制构造函数旨在创建 StrStack 对象的副本,以防函数通过值传入或传出堆栈。函数中的代码很明显,除了下面这一行

next_ = new strNode ( *srcnode.next_ );
                // indirect recursion

这一行是间接递归的一个例子,它复制了 StrStack 节点序列中的所有节点。它是如何工作的?new 的参数是:strNode ( *srcnode.next_ ),这是对节点复制构造函数的另一次调用,其中序列中的下一个节点作为参数。只要每个节点都包含指向另一个节点的指针,复制构造函数就会重复递归调用自身,直到它在序列中最后一个节点的 next_ 字段中遇到 NULL。这样,递归停止并开始展开,从节点序列的尾部向头部反向构造副本。请注意,复制构造函数按承诺处理了两个不同的动态分配:为节点分配内存,然后为保存路径名的字符数组分配内存。在节点析构函数中,行 delete next_ again 触发了一系列隐式递归,这些递归导致析构函数调用自身,直到遇到列表末尾的最终 NULL。此时,递归展开,节点从列表的尾部向头部删除。

更多实用工具

如果 C 库调用完成了大部分繁重的工作,那么编写像 deltree 这样的简单实用程序既不困难也不耗时,并且可能提供意想不到的机会来使用程序的框架来解决其他问题。此代码可以改编为执行与 find 相同的功能;将其用作 findfile 命令的骨架,该命令扫描目录树以查找与命令行参数匹配的文件名。只需将文件和目录删除子例程替换为将每个文件名与目标名称进行比较并打印每个匹配项的路径名的函数即可。

您是否从 Borland 或 Watcom PC DOS 或 OS/2 环境移植代码,并将整个目录树一次性移动到 Linux 空间?如果是这样,那么您可能已经发现不需要的文件与必要的文件一起迁移:带有诸如 .map、.sym、.dsk、.swp、.prj、.exe 等后缀的文件。通过修改,deltree 还可以为 cleandir 实用程序提供框架,该实用程序从工具箱目录树中删除无用信息。要进行必要的更改,请将 StrStack 替换为 StrList 类,该类包含目标文件名后缀列表。该实用程序不是删除所有文件,而是检查目录树中每个文件的后缀是否与目标后缀列表匹配,并有选择地删除。一旦您掌握了遍历 Linux 目录树的窍门,创建插件函数来执行其他任务就变得很简单,并且很容易生成一组实用程序来解决更广泛的目录树维护问题。

实现此命令所需的所有代码都可以通过匿名下载文件 ftp://ftp.linuxjournal.com/pub/lj/listings/issue52/2439.tgz 获得。

Graydon Ekdahl (gekdahl@ibm.net) 是 Econometrics, Inc. 的总裁,该公司位于北卡罗来纳州教堂山。Graydon 喜欢创建数据库应用程序,并且对数据结构、算法、C++ 和 Java 感兴趣。

加载 Disqus 评论