如何在 C 中使用 scandir() 递归列出排序的子目录

问题描述

我正在用 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 之后可以轻松过滤掉这些重复项。