问题描述
我正在用 C 实现部分 Linux ls 命令。我想按字典顺序对目录的内容进行排序,我一直在使用 scandir()
。这对于列出单个目录很容易,但是我在递归列出子目录时遇到了麻烦。我当前的代码:(一旦达到目录类型就会导致分段错误)
void recursive(char* arg){
int i;
struct dirent **file_list;
int num;
char* next_dir;
num = scandir(arg,&file_list,NULL,alphasort);
for(i = 0; i < num; i++) {
if(file_list[i]->d_type == DT_DIR) {
if(strcmp(".",file_list[i]->d_name) != 0 && strcmp("..",file_list[i]->d_name) != 0) {
// Directories are printed with a colon to distinguish them from files
printf("%s: \n",file_list[i]->d_name);
strcpy(next_dir,arg);
strcat(next_dir,"/");
strcat(next_dir,file_list[i]->d_name);
printf("\n");
recursive(next_dir);
}
} else {
if(strcmp(".",file_list[i]->d_name) != 0) {
printf("%s \n",file_list[i]->d_name);
}
}
}
}
int main(void) {
recursive(".");
return 0;
}
解决方法
在 Linux 和其他 POSIXy 系统中,有两种推荐的方法可以遍历整个文件系统树:
-
nftw():
man 3 nftw
给定一个初始路径、一个回调函数、要使用的最大描述符数和一组标志,
nftw()
将为子树中的每个文件系统对象调用一次回调函数。但是,未指定调用同一目录中条目的顺序。这是 POSIX.1 (IEEE 1003) 函数。
-
fts_open()/fts_read()/fts_children()/fts_close():
man 3 fts
fts 接口提供了一种遍历文件系统层次结构的方法。
fts_children()
提供按fts_open()
调用中指定的比较函数排序的文件系统条目的链接列表。它与scandir()
返回文件系统条目数组的方式非常相似,只是两者使用非常不同的结构来描述每个文件系统条目。在 glibc 2.23(2016 年发布)之前,Linux (glibc) fts 实现在使用 64 位文件大小(例如 x86-64 或使用
-D_FILE_OFFSET_BITS=64
编译时)时存在错误。这些是 BSD 函数(FreeBSD/OpenBSD/macOS),但在 Linux 中也可用。
最后,还有一个 atfile 版本的 scandir(),scandirat(),它返回来自特定目录的过滤和排序的文件系统条目,但除了路径名之外,它还需要一个文件描述符到相对用作参数的根目录。 (如果使用 AT_FDCWD
而不是文件描述符,则 scandirat()
的行为类似于 scandir()
。)
这里最简单的选择是使用nftw()
,存储所有走过的路径,最后对路径进行排序。例如,walk.c
:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <locale.h>
#include <ftw.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
struct entry {
/* Insert additional properties like 'off_t size' here. */
char *name; /* Always points to name part of pathname */
char pathname[]; /* Full path and name */
};
struct listing {
size_t max; /* Number of entries allocated for */
size_t num; /* Number of entries in the array */
struct entry **ent; /* Array of pointers,one per entry */
};
#define STRUCT_LISTING_INITIALIZER { 0,NULL }
/* Locale-aware sort for arrays of struct entry pointers.
*/
static int entrysort(const void *ptr1,const void *ptr2)
{
const struct entry *ent1 = *(const struct entry **)ptr1;
const struct entry *ent2 = *(const struct entry **)ptr2;
return strcoll(ent1->pathname,ent2->pathname);
}
/* Global variable used by nftw_add() to add to the listing */
static struct listing *nftw_listing = NULL;
static int nftw_add(const char *pathname,const struct stat *info,int typeflag,struct FTW *ftwbuf)
{
const char *name = pathname + ftwbuf->base;
/* These generate no code,just silences the warnings about unused parameters. */
(void)info;
(void)typeflag;
/* Ignore "." and "..". */
if (name[0] == '.' && !name[1])
return 0;
if (name[0] == '.' && name[1] == '.' && !name[2])
return 0;
/* Make sure there is room for at least one more entry in the listing. */
if (nftw_listing->num >= nftw_listing->max) {
const size_t new_max = nftw_listing->num + 1000;
struct entry **new_ent;
new_ent = realloc(nftw_listing->ent,new_max * sizeof (struct entry *));
if (!new_ent)
return -ENOMEM;
nftw_listing->max = new_max;
nftw_listing->ent = new_ent;
}
const size_t pathnamelen = strlen(pathname);
struct entry *ent;
/* Allocate memory for this entry.
Remember to account for the name,and the end-of-string terminator,'\0',at end of name. */
ent = malloc(sizeof (struct entry) + pathnamelen + 1);
if (!ent)
return -ENOMEM;
/* Copy other filesystem entry properties to ent here; say 'ent->size = info->st_size;'. */
/* Copy pathname,including the end-of-string terminator,'\0'. */
memcpy(ent->pathname,pathname,pathnamelen + 1);
/* The name pointer is always to within the pathname. */
ent->name = ent->pathname + ftwbuf->base;
/* Append. */
nftw_listing->ent[nftw_listing->num++] = ent;
return 0;
}
/* Scan directory tree starting at path,adding the entries to struct listing.
Note: the listing must already have been properly initialized!
Returns 0 if success,nonzero if error; -1 if errno is set to indicate error.
*/
int scan_tree_sorted(struct listing *list,const char *path)
{
if (!list) {
errno = EINVAL;
return -1;
}
if (!path || !*path) {
errno = ENOENT;
return -1;
}
nftw_listing = list;
int result = nftw(path,nftw_add,64,FTW_DEPTH);
nftw_listing = NULL;
if (result < 0) {
errno = -result;
return -1;
} else
if (result > 0) {
errno = 0;
return result;
}
if (list->num > 2)
qsort(list->ent,list->num,sizeof list->ent[0],entrysort);
return 0;
}
int main(int argc,char *argv[])
{
struct listing list = STRUCT_LISTING_INITIALIZER;
setlocale(LC_ALL,"");
if (argc < 2 || !strcmp(argv[1],"-h") || !strcmp(argv[1],"--help")) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr,"\n");
fprintf(stderr,"Usage: %s [ -h | --help ]\n",arg0);
fprintf(stderr," %s .\n"," %s TREE [ TREE ... ]\n","This program lists all files and directories starting at TREE,\n");
fprintf(stderr,"in sorted order.\n");
fprintf(stderr,"\n");
return EXIT_SUCCESS;
}
for (int arg = 1; arg < argc; arg++) {
if (scan_tree_sorted(&list,argv[arg])) {
fprintf(stderr,"%s: Error scanning directory tree: %s.\n",argv[arg],strerror(errno));
return EXIT_FAILURE;
}
}
printf("Found %zu entries:\n",list.num);
for (size_t i = 0; i < list.num; i++)
printf("\t%s\t(%s)\n",list.ent[i]->pathname,list.ent[i]->name);
return EXIT_SUCCESS;
}
使用 gcc -Wall -Wextra -O2 walk.c -o walk
编译,并使用例如运行./walk ..
。
scan_tree_sorted()
函数为指定的目录调用 nftw()
,更新全局变量 nftw_listing
,以便 nftw_add()
回调函数可以向其中添加每个新目录条目。如果列表之后包含多个条目,则使用 qsort()
和区域设置感知比较函数(基于 strcoll()
)对其进行排序。
nftw_add()
跳过 .
和 ..
,并将所有其他路径名添加到列表结构 nftw_listing
。它会根据需要以线性方式自动增长数组; new_max = nftw_listing->num + 1000;
表示我们以千(指针)为单位进行分配。
如果想要在一个列表中列出不相交的子树,可以使用与目标相同的列表多次调用 scan_tree_sorted()
。但是请注意,它不会检查重复项,尽管在 qsort 之后可以轻松过滤掉这些重复项。