3.8 扫描目录
Linux系统上一个常见问题就是扫描目录,也就是确定一个特定目录下存放的文件。在shell程序设计中,这很容易做到——只需让shell做一次表达式的通配符扩展。在过去,UNIX操作系统的各种变体都允许用户通过编程访问底层文件系统结构。你仍然可以把目录当作一个普通文件那样打开,并直接读取目录数据项,但不同的文件系统结构及其实现已经使这种方法没什么可移植性了。现在,一整套标准的库函数已经被开发出来,使得目录的扫描工作变得简单多了。
与目录操作有关的函数在dirent.h头文件中声明。它们使用一个名为DIR的结构作为目录操作的基础。被称为目录流的指向这个结构的指针(DIR *)被用来完成各种目录操作,其使用方法与用来操作普通文件的文件流(FILE *)非常相似。目录数据项本身则在dirent结构中返回,该结构也是在dirent.h头文件里声明的,这是因为用户不应直接改动DIR结构中的数据字段。
我们将介绍下面这几个函数:
❑ opendir
❑ readdir
❑ telldir
❑ seekdir
❑ closedir
3.8.1 opendir函数
opendir函数的作用是打开一个目录并建立一个目录流。如果成功,它返回一个指向DIR结构的指针,该指针用于读取目录数据项。
opendir在失败时返回一个空指针。注意,目录流使用一个底层文件描述符来访问目录本身,所以如果打开的文件过多,opendir可能会失败。
3.8.2 readdir函数
readdir函数返回一个指针,该指针指向的结构里保存着目录流dirp中下一个目录项的有关资料。后续的readdir调用将返回后续的目录项。如果发生错误或者到达目录尾,readdir将返回NULL。POSIX兼容的系统在到达目录尾时会返回NULL,但并不改变errno的值,只有在发生错误时才会设置errno。
注意,如果在readdir函数扫描目录的同时还有其他进程在该目录里创建或删除文件,readdir将不保证能够列出该目录里的所有文件(和子目录)。
dirent结构中包含的目录项内容包括以下部分。
❑ ino_t d_ino:文件的inode节点号。
❑ char d_name[]:文件的名字。
要想进一步了解目录中某个文件,你需要使用在本章前面介绍过的stat调用。
3.8.3 telldir函数
telldir函数的返回值记录着一个目录流里的当前位置。你可以在随后的seekdir调用中利用这个值来重置目录扫描到当前位置。
3.8.4 seekdir函数
seekdir函数的作用是设置目录流dirp的目录项指针。loc的值用来设置指针位置,它应该通过前一个telldir调用获得。
3.8.5 closedir函数
closedir函数关闭一个目录流并释放与之关联的资源。它在执行成功时返回0,发生错误时返回-1。
在下面的printdir.c程序中,你将把许多文件处理函数集中在一起以实现一个简单的目录列表功能。目录中的每个文件单独列在一行上。每个子目录会在它的名字后面加上一个斜线字符/,子目录中的文件在缩进四个空格后依次排列。
程序会逐个切换到每个下级子目录里,这样使它找到的文件都有一个可用的名字。也就是说,它们都可以被直接传递给opendir函数。如果目录的嵌套层次太深,程序执行就会失败,这是因为对允许打开的目录流数目是有限制的。
我们当然可以采取一个更通用的做法,让程序能够通过一个命令行参数来指定起点(从哪个目录开始)。请查阅有关工具程序(如ls和find)的Linux源代码来找到实现更通用程序的方法。
实验 一个目录扫描程序
(1)程序的开始是一些必要的头文件。接下来是printdir函数,它的作用是输出当前目录的内容。它将递归遍历各级子目录,使用depth参数来控制缩排。
(2)下面是main函数:
这个程序扫描home目录并产生如下所示的输出(经过简化)。如果想扫描其他用户的目录,你可能需要超级用户的权限。
实验解析
绝大部分操作都是在printdir函数里完成的。在用opendir函数检查完指定目录是否存在后,printdir调用chdir进入指定目录。如果readdir函数返回的数据项不为空,程序就检查该数据项是否是一个目录。如果不是,程序就根据depth的值缩进打印该文件数据项的内容。
如果该数据项是一个目录,你就需要对它进行递归遍历。在跳过.和..数据项(它们分别代表当前目录和上一级目录)后,printdir函数调用自己并再次进入一个同样的处理过程。那它又是如何退出这些循环的呢?一旦while循环完成,chdir("..")调用将把它带回到目录树的上一级,从而可以继续进行上级目录的遍历。调用closedir(dp)关闭目录是为了确保打开的目录流数目不超出其需要。
作为对第4章所介绍的Linux环境的一个简短尝试,让我们来看一个能够使这个程序更具通用性的方法。这个程序的功能受限是因为它只能对目录/home进行操作。如果我们按下面的方法对main函数进行修改,就能把它变成一个更有用的目录浏览器:
我们修改了3条语句,增加了5条语句,它现在是一个通用的工具程序了。它多了一个可选的目录名参数,其默认值是当前目录。你可以通过下面的命令运行它:
输出结果将分页显示,用户可以通过翻页查看其输出。可以说,用户现在有了一个非常方便、通用的目录树浏览器。你不必花费过多精力就可以为这个程序再增加空间占用统计、遍历深度限制等其他功能。