Linux的目录是一个树状结构,了解数据结构的都明白,遍历一棵树最简单的方式是递归。 掌握递归的使用方法,遍历树状目录也不难。
Linux给我们提供了相关的目录遍历的函数,分别为:opendir()
, readdir()
, closedir()
。
目录的操作方式和标准C库提供的文件操作步骤是类似的。
1. 目录三剑客
1.1 opendir
在目录操作之前必须要先通过 opendir()
函数打开这个目录
// 函数原型 #include <sys/types.h> #include <dirent.h> // 打开目录 DIR *opendir(const char *name);
- 参数: name -> 要打开的目录的名字
- 返回值: DIR*, 结构体类型指针。打开成功返回目录的实例,打开失败返回 NULL
1.2 readdir
目录打开之后,就可以通过 readdir()
函数遍历目录中的文件信息了。
每调用一次这个函数就可以得到目录中的一个文件信息
当目录中的文件信息被全部遍历完毕会得到一个空对象。
// 函数原型 // 读目录 #include <dirent.h> struct dirent *readdir(DIR *dirp);
- 参数:dirp -> opendir() 函数的返回值
- 返回值:函数调用成功,返回读到的文件的信息, 目录文件被读完了或者函数调用失败返回 NULL
函数返回值 struct dirent
结构体原型如下:
struct dirent { ino_t d_ino; /* 文件对应的inode编号, 定位文件存储在磁盘的那个数据块上 */ off_t d_off; /* 文件在当前目录中的偏移量 */ unsigned short d_reclen; /* 文件名字的实际长度 */ unsigned char d_type; /* 文件的类型, linux中有7中文件类型 */ char d_name[256]; /* 文件的名字 */ };
关于结构体中的文件类型d_type
,可使用的宏值如下:
-
DT_BLK
:块设备文件 -
DT_CHR
:字符设备文件 -
DT_DIR
:目录文件 -
DT_FIFO
:管道文件 -
DT_LNK
:软连接文件 -
DT_REG
:普通文件 -
DT_SOCK
:本地套接字文件 -
DT_UNKNOWN
:无法识别的文件类型
那么,如何通过 readdir() 函数遍历某一个目录中的文件呢?
// 打开目录 DIR* dir = opendir("/home/test"); struct dirent* ptr = NULL; // 遍历目录 while( (ptr=readdir(dir)) != NULL) { ....... }
1.3 closedir
目录操作完毕之后, 需要通过 closedir()
关闭通过opendir()
得到的实例,释放资源。
// 函数原型 // 关闭目录, 参数是 opendir() 的返回值 int closedir(DIR *dirp);
- 参数:dirp-> opendir() 函数的返回值
- 返回值: 目录关闭成功返回0, 失败返回 -1
2. 遍历目录
2.1 遍历单层目录
如果只遍历单层目录是不需要递归的,按照上边介绍的函数的使用方法,依次继续调用即可。 假设我们需要得到某个指定目录下 mp3 格式文件的个数
// filenum.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <dirent.h> int main(int argc, char* argv[]) { // 1. 打开目录 DIR* dir = opendir(argv[1]); if(dir == NULL) { perror("opendir"); return -1; } // 2. 遍历当前目录中的文件 int count = 0; while(1) { struct dirent* ptr = readdir(dir); if(ptr == NULL) { printf("目录读完了...\n"); break; } // 读到了一个文件 // 判断文件类型 if(ptr->d_type == DT_REG) { char* p = strstr(ptr->d_name, ".mp3"); if(p != NULL && *(p+4) == '\0') //+4 判断是否真的是后缀 { count++; printf("file %d: %s\n", count, ptr->d_name); } } } printf("%s目录中mp3文件的个数: %d\n", argv[1], count); // 关闭目录 closedir(dir); return 0; }
编译名执行程序
$ gcc filenum.c # 读当前目录中mp3文件个数 $ ./a.out . file 1: 1.mp3 目录读完了... .目录中mp3文件的个数: 1 # 读 ./sub 目录中mp3文件个数 $ ./a.out ./sub/ file 1: 3.mp3 file 2: 1.mp3 file 3: 5.mp3 file 4: 4.mp3 file 5: 2.mp3 目录读完了... ./sub/目录中mp3文件的个数: 5
2.2 遍历多层目录
Linux 的目录是树状结构,遍历每层目录的方式是一样的,也就是说最简单的遍历方式是递归。 程序的重点就是确定递归结束的条件:遍历的文件如果不是目录类型就结束递归。
示例代码如下:
// filenum.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <dirent.h> int getMp3Num(const char* path) { // 1. 打开目录 DIR* dir = opendir(path); if(dir == NULL) { perror("opendir"); return 0; } // 2. 遍历当前目录 struct dirent* ptr = NULL; int count = 0; while((ptr = readdir(dir)) != NULL) { // 如果是目录 . .. 跳过不处理 if(strcmp(ptr->d_name, ".")==0 || strcmp(ptr->d_name, "..") == 0) { continue; } // 假设读到的当前文件是目录 if(ptr->d_type == DT_DIR) { // 目录 char newPath[1024]; sprintf(newPath, "%s/%s", path, ptr->d_name); // 读当前目录的子目录 count += getMp3Num(newPath); } else if(ptr->d_type == DT_REG) { // 普通文件 char* p = strstr(ptr->d_name, ".mp3"); // 判断文件后缀是不是 .mp3 if(p != NULL && *(p+4) == '\0') { count++; printf("%s/%s\n", path, ptr->d_name); } } } closedir(dir); return count; } int main(int argc, char* argv[]) { // ./a.out path if(argc < 2) { printf("./a.out path\n"); return 0; } int num = getMp3Num(argv[1]); printf("%s 目录中mp3文件个数: %d\n", argv[1], num); return 0; }
编译并运行程序:
$ gcc filenum.c # 查看 abc 目录中mp3 文件个数 $ ./a.out abc abc/sub/3.mp3 abc/sub/1.mp3 abc/sub/5.mp3 abc/sub/4.mp3 abc/sub/2.mp3 abc/sub/music/test2.mp3 abc/sub/music/test3.mp3 abc/sub/music/test1.mp3 abc/hello.mp3 abc 目录中mp3文件个数: 9
3. scandir函数
也可使用scandir()
函数进行目录的遍历(只遍历指定目录,不进入到子目录中进行递归遍历)
它的参数并不简单,涉及到三级指针和回调函数的使用。
/
/ 函数原型 // 头文件 #include <dirent.h> int scandir(const char *dirp, struct dirent ***namelist, int (*filter)(const struct dirent *), int (*compar)(const struct dirent **, const struct dirent **)); int alphasort(const struct dirent **a, const struct dirent **b); int versionsort(const struct dirent **a, const struct dirent **b);
- 参数:
- dirp: 需要遍历的目录的名字
- namelist: 三级指针, 传出参数, 需要在指向的地址中存储遍历目录得到的所有文件的信息
在函数内部会给这个指针指向的地址分配内存,要注意在程序中释放内存
- filter: 函数指针, 指针指向的函数就是回调函数, 需要在自定义函数中指定如果过滤目录中的文件
- 如果不对目录中的文件进行过滤, 该函数指针指定为NULL即可
- 如果自己指定过滤函数, 满足条件要返回1, 否则返回 0
- compar: 函数指针, 对过滤得到的文件进行排序, 可以使用提供的两种排序方式:
- alphasort: 根据文件名进行排序
- versionsort: 根据版本进行排序
- 返回值: 函数执行成功返回找到的匹配成功的文件的个数,如果失败返回-1。
3.1 文件过滤
scandir()
可以让使用者自定义文件的过滤方式, 然后将过滤函数的地址传递给 scandir()
的第三个参数
// 过滤函数的原型 // 函数的参数就是遍历的目录中的子文件对应的结构体 int (*filter)(const struct dirent *);
基于这个函数指针定义的函数就可以称之为回调函数, 这个函数不是由我们调用, 而是通过
scandir()
调用,因此这个函数的实参也是由scandir()
函数提供的,作为回调函数的编写人员,只需搞清这个参数的含义是什么,然后在函数体中直接使用即可。
假设判断目录中某一个文件是否为Mp3格式
int isMp3(const struct dirent *ptr) { if(ptr->d_type == DT_REG) { char* p = strstr(ptr->d_name, ".mp3"); if(p != NULL && *(p+4) == '\0') { return 1; } } return 0; }
3.2 遍历目录
了解了 scandir()
函数的使用后,
写一个程序, 搜索指定目录下的 mp3格式文件个数和文件名
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <dirent.h> // 文件过滤函数 int isMp3(const struct dirent *ptr) { if(ptr->d_type == DT_REG) { char* p = strstr(ptr->d_name, ".mp3"); if(p != NULL && *(p+4) == '\0') { return 1; } } return 0; } int main(int argc, char* argv[]) { if(argc < 2) { printf("./a.out path\n"); return 0; } struct dirent **namelist = NULL; int num = scandir(argv[1], &namelist, isMp3, alphasort); for(int i=0; i<num; ++i) { printf("file %d: %s\n", i, namelist[i]->d_name); free(namelist[i]); } free(namelist); return 0; }
再解析一下 scandir() 的第二个参数,传递的是一个二级指针的地址:
struct dirent **namelist = NULL; int num = scandir(argv[1], &namelist, isMp3, alphasort);
那么在这个 namelist
中存储的什么类型的数据呢?
也就是 struct dirent **namelist
指向的什么类型的数据?
答案: 指向的是一个指针数组 struct dirent *namelist[]
- 数组元素的个数就是遍历的目录中的文件个数
- 数组的每个元素都是指针类型:
struct dirent *
指针指向的地址是由scandir()
函数分配的, 因此在使用完毕之后需要释放内存