如何在 Linux 上递归列出 C 中的目录?
Posted
技术标签:
【中文标题】如何在 Linux 上递归列出 C 中的目录?【英文标题】:How to recursively list directories in C on Linux? 【发布时间】:2012-01-16 05:38:17 【问题描述】:我需要递归列出 C 编程中的所有目录和文件。我已经研究过 FTW,但我使用的 2 个操作系统(Fedora 和 Minix)不包括它。我开始对过去几个小时阅读的所有不同内容感到头疼。
如果有人知道代码 sn-p 我可以看看那将是惊人的,或者如果有人可以在这方面给我很好的指导,我将非常感激。
【问题讨论】:
为什么不直接用脚本语言来做呢?这样写起来会更快更容易。 @dbeer 如果他在 C 程序中需要这些信息怎么办? 您确定要递归执行该操作吗?我要指出,循环链接和打开文件限制可能会给递归实现带来问题。我会考虑使用一个(或两个)链表,这样代码就可以检查以前处理过的文件夹。这也将允许代码在遍历深层层次结构时使用单个打开的文件。 【参考方案1】:为什么每个人都坚持一次又一次地重新发明***?
POSIX.1-2008 标准化了 nftw()
函数,该函数也在单一 Unix 规范 v4 (SuSv4) 中定义,可在 Linux(glibc、man 3 nftw
)、OS X 和大多数最新的 BSD 变体中使用。一点都不新鲜。
Naïve opendir()
/readdir()
/closedir()
基于实现几乎从不处理目录或文件在树遍历期间被移动、重命名或删除的情况,而 nftw()
应该优雅地处理它们。
例如,考虑以下 C 程序,它列出了从当前工作目录开始的目录树,或者在命令行命名的每个目录,或者只是在命令行命名的文件:
/* We want POSIX.1-2008 + XSI, i.e. SuSv4, features */
#define _XOPEN_SOURCE 700
/* Added on 2017-06-25:
If the C library can support 64-bit file sizes
and offsets, using the standard names,
these defines tell the C library to do so. */
#define _LARGEFILE64_SOURCE
#define _FILE_OFFSET_BITS 64
#include <stdlib.h>
#include <unistd.h>
#include <ftw.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
/* POSIX.1 says each process has at least 20 file descriptors.
* Three of those belong to the standard streams.
* Here, we use a conservative estimate of 15 available;
* assuming we use at most two for other uses in this program,
* we should never run into any problems.
* Most trees are shallower than that, so it is efficient.
* Deeper trees are traversed fine, just a bit slower.
* (Linux allows typically hundreds to thousands of open files,
* so you'll probably never see any issues even if you used
* a much higher value, say a couple of hundred, but
* 15 is a safe, reasonable value.)
*/
#ifndef USE_FDS
#define USE_FDS 15
#endif
int print_entry(const char *filepath, const struct stat *info,
const int typeflag, struct FTW *pathinfo)
/* const char *const filename = filepath + pathinfo->base; */
const double bytes = (double)info->st_size; /* Not exact if large! */
struct tm mtime;
localtime_r(&(info->st_mtime), &mtime);
printf("%04d-%02d-%02d %02d:%02d:%02d",
mtime.tm_year+1900, mtime.tm_mon+1, mtime.tm_mday,
mtime.tm_hour, mtime.tm_min, mtime.tm_sec);
if (bytes >= 1099511627776.0)
printf(" %9.3f TiB", bytes / 1099511627776.0);
else
if (bytes >= 1073741824.0)
printf(" %9.3f GiB", bytes / 1073741824.0);
else
if (bytes >= 1048576.0)
printf(" %9.3f MiB", bytes / 1048576.0);
else
if (bytes >= 1024.0)
printf(" %9.3f KiB", bytes / 1024.0);
else
printf(" %9.0f B ", bytes);
if (typeflag == FTW_SL)
char *target;
size_t maxlen = 1023;
ssize_t len;
while (1)
target = malloc(maxlen + 1);
if (target == NULL)
return ENOMEM;
len = readlink(filepath, target, maxlen);
if (len == (ssize_t)-1)
const int saved_errno = errno;
free(target);
return saved_errno;
if (len >= (ssize_t)maxlen)
free(target);
maxlen += 1024;
continue;
target[len] = '\0';
break;
printf(" %s -> %s\n", filepath, target);
free(target);
else
if (typeflag == FTW_SLN)
printf(" %s (dangling symlink)\n", filepath);
else
if (typeflag == FTW_F)
printf(" %s\n", filepath);
else
if (typeflag == FTW_D || typeflag == FTW_DP)
printf(" %s/\n", filepath);
else
if (typeflag == FTW_DNR)
printf(" %s/ (unreadable)\n", filepath);
else
printf(" %s (unknown)\n", filepath);
return 0;
int print_directory_tree(const char *const dirpath)
int result;
/* Invalid directory path? */
if (dirpath == NULL || *dirpath == '\0')
return errno = EINVAL;
result = nftw(dirpath, print_entry, USE_FDS, FTW_PHYS);
if (result >= 0)
errno = result;
return errno;
int main(int argc, char *argv[])
int arg;
if (argc < 2)
if (print_directory_tree("."))
fprintf(stderr, "%s.\n", strerror(errno));
return EXIT_FAILURE;
else
for (arg = 1; arg < argc; arg++)
if (print_directory_tree(argv[arg]))
fprintf(stderr, "%s.\n", strerror(errno));
return EXIT_FAILURE;
return EXIT_SUCCESS;
上面的大部分代码都在print_entry()
中。它的任务是打印出每个目录条目。在print_directory_tree()
中,我们告诉nftw()
为它看到的每个目录条目调用它。
上面唯一的手动细节是关于应该让nftw()
使用多少个文件描述符的决定。如果您的程序在文件树遍历期间最多使用两个额外的文件描述符(除了标准流之外),则已知 15 是安全的(在所有具有nftw()
并且大部分符合 POSIX 的系统上)。
在 Linux 中,您可以使用 sysconf(_SC_OPEN_MAX)
来查找打开文件的最大数量,并减去与 nftw()
调用同时使用的数量,但我不会打扰(除非我知道会使用该实用程序大多具有病态深层目录结构)。十五个描述符不限制树的深度; nftw()
只是变得更慢(并且如果从目录中遍历比 13 个目录更深的目录,则可能无法检测到目录中的更改,尽管权衡和检测更改的一般能力在系统和 C 库实现之间有所不同)。仅仅使用这样的编译时常量就可以保持代码的可移植性——它不仅可以在 Linux 上工作,而且可以在 Mac OS X 和所有当前的 BSD 变体以及大多数其他不太旧的 Unix 变体上工作。
在评论中,Ruslan 提到他们必须切换到 nftw64()
,因为他们有需要 64 位大小/偏移量的文件系统条目,而 nftw()
的“正常”版本以 errno == EOVERFLOW
失败。正确的解决方案是不要切换到 GLIBC 特定的 64 位函数,而是定义 _LARGEFILE64_SOURCE
和 _FILE_OFFSET_BITS 64
。这些告诉 C 库尽可能切换到 64 位文件大小和偏移量,同时使用标准函数(nftw()
、fstat()
等)和类型名称(off_t
等)。
【讨论】:
重新发明这个特殊的***的主要原因是它被设置为使用opendir()
、readdir()
、closedir()
的练习,目的是教人们仔细思考关于完整路径名与目录条目。在生产代码中,nftw()
是要走的路——不要误会我的意思。但是,使用原始函数进行练习还有一个教学原因。
@moodboom:整个文件系统本质上是一个非线程安全的全局,很严重。仅仅因为 A Coding Guru 说全局变量很恶心并不意味着它们没有有效用途。 正确遍历目录树并不简单,而且我见过的大多数声称正确的示例都像我内心的花朵一样脆弱。非常脆弱。如果它是在 POSIX 中定义的,我会使用fts,但可惜它只是 4.4BSD,而且支持很分散。即使在 GNU C 库中,支持(例如对大文件)也不确定高达 2.23。链接的讨论很愚蠢。
@moodboom:对。请注意,我回答的关键点是使用opendir()
/readdir()
/closedir()
的典型示例是不安全的并且容易出现竞争,并且如果您在步行时移动所涉及的目录可能会做一些非常奇怪的事情进步。这是糟糕的代码。脆弱的。 nftw()
等。应该正确处理所有这些。我们可以使用 POSIX.1-2008 openat()
/fdopendir()
/readdir()
/closedir()
编写一个安全的目录树遍历器,但是我们可以做的树深度将受到我们可以使用的文件描述符数量的限制。 nftw()
甚至应该安全地避免这种情况。
@gsamaras: :) 我在 cboard 认识你;我们已经通过电子邮件发送了所有内容,谈到了笼子比赛等等。如果您丢失了我的电子邮件地址(我不再使用 cboard - 我的答案显然不需要,太长且太详细而无法实际阅读),您总是可以通过my home page 找到我的电子邮件地址。
在看到这篇文章之前,我什至不知道*dir
函数的替代品。谢谢一百万。【参考方案2】:
这是一个递归版本:
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
void listdir(const char *name, int indent)
DIR *dir;
struct dirent *entry;
if (!(dir = opendir(name)))
return;
while ((entry = readdir(dir)) != NULL)
if (entry->d_type == DT_DIR)
char path[1024];
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
snprintf(path, sizeof(path), "%s/%s", name, entry->d_name);
printf("%*s[%s]\n", indent, "", entry->d_name);
listdir(path, indent + 2);
else
printf("%*s- %s\n", indent, "", entry->d_name);
closedir(dir);
int main(void)
listdir(".", 0);
return 0;
【讨论】:
应该在int is_directory_we_want_to_list(const char *parent, char *name)
struct stat st_buf;
if (!strcmp(".", name) || !strcmp("..", name))
return 0;
char *path = alloca(strlen(name) + strlen(parent) + 2);
sprintf(path, "%s/%s", parent, name);
stat(path, &st_buf);
return S_ISDIR(st_buf.st_mode);
int list(const char *name)
DIR *dir = opendir(name);
struct dirent *ent;
while (ent = readdir(dir))
char *entry_name = ent->d_name;
printf("%s\n", entry_name);
if (is_directory_we_want_to_list(name, entry_name))
// You can consider using alloca instead.
char *next = malloc(strlen(name) + strlen(entry_name) + 2);
sprintf(next, "%s/%s", name, entry_name);
list(next);
free(next);
closedir(dir);
在这种情况下值得略读的头文件:stat.h、dirent.h。请记住,上面的代码不会检查可能发生的任何错误。
ftw.h 中定义的ftw
提供了一种完全不同的方法。
【讨论】:
你没有递归。至少看起来不像。您的意思是在评论下方致电list(entry_name)
吗?
@Jon,这是真的,我只是写了一个骨架来帮助 OP 开始。如果还不够,我可以详细说明。
这样会列出目录中的所有文件以及所有子目录以及子目录中的所有文件?
这是一个 sn-p 的开始。他在 cmets 中添加了其他需要添加的内容。正如发布的那样,没有检查文件与目录,或者确保目录不是.
或..
,也没有递归调用,所以我认为它只会打印给定目录中的文件。
我已经更新了答案。现在它列出了给定目录的内容并递归到is_directory_we_want_to_list
返回非零值的子目录。 HTH。【参考方案4】:
正如我在评论中提到的,我认为递归方法对这项任务有两个固有的缺陷。
第一个缺陷是打开文件的限制。此限制对深度遍历施加了限制。如果有足够的子文件夹,递归方法就会中断。 (查看有关堆栈溢出的编辑)
第二个缺陷有点微妙。递归方法使得测试硬链接变得非常困难。如果文件夹树是循环的(由于硬链接),递归方法将中断(希望没有堆栈溢出)。 (查看有关硬链接的编辑)
但是,通过将递归替换为单个文件描述符和链表来避免这些问题非常简单。
我认为这不是一个学校项目,递归是可选的。
这是一个示例应用程序。
使用a.out ./
查看文件夹树。
我为宏和其他东西道歉...我通常使用内联函数,但我认为如果代码全部在一个函数中会更容易遵循。
#include <dirent.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
/* print use instruction unless a folder name was given */
if (argc < 2)
fprintf(stderr,
"\nuse:\n"
" %s <directory>\n"
"for example:\n"
" %s ./\n\n",
argv[0], argv[0]),
exit(0);
/*************** a small linked list macro implementation ***************/
typedef struct list_s
struct list_s *next;
struct list_s *prev;
list_s;
#define LIST_INIT(name) \
.next = &name, .prev = &name
#define LIST_PUSH(dest, node) \
do \
(node)->next = (dest)->next; \
(node)->prev = (dest); \
(node)->next->prev = (node); \
(dest)->next = (node); \
while (0);
#define LIST_POP(list, var) \
if ((list)->next == (list)) \
var = NULL; \
else \
var = (list)->next; \
(list)->next = var->next; \
var->next->prev = var->prev; \
/*************** a record (file / folder) item type ***************/
typedef struct record_s
/* this is a flat processing queue. */
list_s queue;
/* this will list all queued and processed folders (cyclic protection) */
list_s folders;
/* this will list all the completed items (siblings and such) */
list_s list;
/* unique ID */
ino_t ino;
/* name length */
size_t len;
/* name string */
char name[];
record_s;
/* take a list_s pointer and convert it to the record_s pointer */
#define NODE2RECORD(node, list_name) \
((record_s *)(((uintptr_t)(node)) - \
((uintptr_t) & ((record_s *)0)->list_name)))
/* initializes a new record */
#define RECORD_INIT(name) \
(record_s).queue = LIST_INIT((name).queue), \
.folders = LIST_INIT((name).folders), \
.list = LIST_INIT((name).list)
/*************** the actual code ***************/
record_s records = RECORD_INIT(records);
record_s *pos, *item;
list_s *tmp;
DIR *dir;
struct dirent *entry;
/* initialize the root folder record and add it to the queue */
pos = malloc(sizeof(*pos) + strlen(argv[1]) + 2);
*pos = RECORD_INIT(*pos);
pos->len = strlen(argv[1]);
memcpy(pos->name, argv[1], pos->len);
if (pos->name[pos->len - 1] != '/')
pos->name[pos->len++] = '/';
pos->name[pos->len] = 0;
/* push to queue, but also push to list (first item processed) */
LIST_PUSH(&records.queue, &pos->queue);
LIST_PUSH(&records.list, &pos->list);
/* as long as the queue has items to be processed, do so */
while (records.queue.next != &records.queue)
/* pop queued item */
LIST_POP(&records.queue, tmp);
/* collect record to process */
pos = NODE2RECORD(tmp, queue);
/* add record to the processed folder list */
LIST_PUSH(&records.folders, &pos->folders);
/* process the folder and add all folder data to current list */
dir = opendir(pos->name);
if (!dir)
continue;
while ((entry = readdir(dir)) != NULL)
/* create new item, copying it's path data and unique ID */
item = malloc(sizeof(*item) + pos->len + entry->d_namlen + 2);
*item = RECORD_INIT(*item);
item->len = pos->len + entry->d_namlen;
memcpy(item->name, pos->name, pos->len);
memcpy(item->name + pos->len, entry->d_name, entry->d_namlen);
item->name[item->len] = 0;
item->ino = entry->d_ino;
/* add item to the list, right after the `pos` item */
LIST_PUSH(&pos->list, &item->list);
/* unless it's a folder, we're done. */
if (entry->d_type != DT_DIR)
continue;
/* test for '.' and '..' */
if (entry->d_name[0] == '.' &&
(entry->d_name[1] == 0 ||
(entry->d_name[1] == '.' && entry->d_name[2] == 0)))
continue;
/* add folder marker */
item->name[item->len++] = '/';
item->name[item->len] = 0;
/* test for cyclic processing */
list_s *t = records.folders.next;
while (t != &records.folders)
if (NODE2RECORD(t, folders)->ino == item->ino)
/* we already processed this folder! */
break; /* this breaks from the small loop... */
t = t->next;
if (t != &records.folders)
continue; /* if we broke from the small loop, entry is done */
/* item is a new folder, add to queue */
LIST_PUSH(&records.queue, &item->queue);
closedir(dir);
/*************** Printing the results and cleaning up ***************/
while (records.list.next != &records.list)
/* pop list item */
LIST_POP(&records.list, tmp);
/* collect and process record */
pos = NODE2RECORD(tmp, list);
fwrite(pos->name, pos->len, 1, stderr);
fwrite("\n", 1, 1, stderr);
/* free node */
free(pos);
return 0;
编辑
@Stargateur 在 cmets 中提到,递归代码可能会在达到打开文件限制之前溢出堆栈。
虽然我看不出堆栈溢出有什么更好的地方,但只要进程在调用时不接近文件限制,这种评估可能是正确的。
@Stargateur 在 cmets 中提到的另一点是递归代码的深度受到最大子目录数量(ext4 文件系统上为 64000)的限制,并且硬链接极不可能(因为硬链接到文件夹不允许在 Linux/Unix 上使用)。
如果代码在 Linux 上运行(根据问题是这样),这是个好消息,所以这个问题不是一个真正的问题(除非在 macOS 或 Windows 上运行代码)...尽管递归中的 64K 子文件夹可能会使堆栈大开。
话虽如此,非递归选项仍然具有优势,例如能够轻松地对处理的项目数量添加限制以及能够缓存结果。
附言
根据 cmets,这是不检查循环层次结构的代码的非递归版本。它速度更快,并且应该足够安全,可以在不允许硬链接到文件夹的 Linux 机器上使用。
#include <dirent.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
/* print use instruction unless a folder name was given */
if (argc < 2)
fprintf(stderr,
"\nuse:\n"
" %s <directory>\n"
"for example:\n"
" %s ./\n\n",
argv[0], argv[0]),
exit(0);
/*************** a small linked list macro implementation ***************/
typedef struct list_s
struct list_s *next;
struct list_s *prev;
list_s;
#define LIST_INIT(name) \
.next = &name, .prev = &name
#define LIST_PUSH(dest, node) \
do \
(node)->next = (dest)->next; \
(node)->prev = (dest); \
(node)->next->prev = (node); \
(dest)->next = (node); \
while (0);
#define LIST_POP(list, var) \
if ((list)->next == (list)) \
var = NULL; \
else \
var = (list)->next; \
(list)->next = var->next; \
var->next->prev = var->prev; \
/*************** a record (file / folder) item type ***************/
typedef struct record_s
/* this is a flat processing queue. */
list_s queue;
/* this will list all the completed items (siblings and such) */
list_s list;
/* unique ID */
ino_t ino;
/* name length */
size_t len;
/* name string */
char name[];
record_s;
/* take a list_s pointer and convert it to the record_s pointer */
#define NODE2RECORD(node, list_name) \
((record_s *)(((uintptr_t)(node)) - \
((uintptr_t) & ((record_s *)0)->list_name)))
/* initializes a new record */
#define RECORD_INIT(name) \
(record_s).queue = LIST_INIT((name).queue), .list = LIST_INIT((name).list)
/*************** the actual code ***************/
record_s records = RECORD_INIT(records);
record_s *pos, *item;
list_s *tmp;
DIR *dir;
struct dirent *entry;
/* initialize the root folder record and add it to the queue */
pos = malloc(sizeof(*pos) + strlen(argv[1]) + 2);
*pos = RECORD_INIT(*pos);
pos->len = strlen(argv[1]);
memcpy(pos->name, argv[1], pos->len);
if (pos->name[pos->len - 1] != '/')
pos->name[pos->len++] = '/';
pos->name[pos->len] = 0;
/* push to queue, but also push to list (first item processed) */
LIST_PUSH(&records.queue, &pos->queue);
LIST_PUSH(&records.list, &pos->list);
/* as long as the queue has items to be processed, do so */
while (records.queue.next != &records.queue)
/* pop queued item */
LIST_POP(&records.queue, tmp);
/* collect record to process */
pos = NODE2RECORD(tmp, queue);
/* process the folder and add all folder data to current list */
dir = opendir(pos->name);
if (!dir)
continue;
while ((entry = readdir(dir)) != NULL)
/* create new item, copying it's path data and unique ID */
item = malloc(sizeof(*item) + pos->len + entry->d_namlen + 2);
*item = RECORD_INIT(*item);
item->len = pos->len + entry->d_namlen;
memcpy(item->name, pos->name, pos->len);
memcpy(item->name + pos->len, entry->d_name, entry->d_namlen);
item->name[item->len] = 0;
item->ino = entry->d_ino;
/* add item to the list, right after the `pos` item */
LIST_PUSH(&pos->list, &item->list);
/* unless it's a folder, we're done. */
if (entry->d_type != DT_DIR)
continue;
/* test for '.' and '..' */
if (entry->d_name[0] == '.' &&
(entry->d_name[1] == 0 ||
(entry->d_name[1] == '.' && entry->d_name[2] == 0)))
continue;
/* add folder marker */
item->name[item->len++] = '/';
item->name[item->len] = 0;
/* item is a new folder, add to queue */
LIST_PUSH(&records.queue, &item->queue);
closedir(dir);
/*************** Printing the results and cleaning up ***************/
while (records.list.next != &records.list)
/* pop list item */
LIST_POP(&records.list, tmp);
/* collect and process record */
pos = NODE2RECORD(tmp, list);
fwrite(pos->name, pos->len, 1, stderr);
fwrite("\n", 1, 1, stderr);
/* free node */
free(pos);
return 0;
【讨论】:
@Stargateur ,如果递归代码在服务器上运行,那么您可能是对的——尽管在这种情况下,保持堆栈完整并尽量减少文件描述符的使用可能是一个优先事项。此外,如果存在循环文件夹层次结构,则子目录限制不会拯救你......但是,链表方法可以防止堆栈溢出,并且它永远不会挂起或崩溃(即使在处理大树时)。对处理的项目数量添加限制也很容易。我喜欢递归代码,因为它很简单,但使用起来通常更危险,而且会威胁到堆栈。 @Stargateur 硬链接怎么样...?它们不是风险,还是操作系统总是阻止硬链接创建循环层次结构? macOS 并没有正式允许它们,但它们在 TimeMachine 中使用并且可以创建(虽然不是由外行人)......所以我知道一个系统可能会弹出这些硬链接(要么因为它们是创建的,或者因为代码在包含 TimeMachine 备份的文件夹中运行)...但是,您可能是对的,风险不大。 @Stargateur - 我更新了答案以反映您的 cmets。 在最后一个循环中,LIST_POP(&records.list, tmp) 宏的第二次使用是弹出我认为调用者实际上想要打印的项目。我发现 both LIST_POP() 使用时,会跳过几个“叶”文件项。当我删除第二个 LIST_POP() 实例时,程序会打印我期望的文件。【参考方案5】:这是一个简化的递归版本,但使用的堆栈空间要少得多:
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>
void listdir(char *path, size_t size)
DIR *dir;
struct dirent *entry;
size_t len = strlen(path);
if (!(dir = opendir(path)))
fprintf(stderr, "path not found: %s: %s\n",
path, strerror(errno));
return;
puts(path);
while ((entry = readdir(dir)) != NULL)
char *name = entry->d_name;
if (entry->d_type == DT_DIR)
if (!strcmp(name, ".") || !strcmp(name, ".."))
continue;
if (len + strlen(name) + 2 > size)
fprintf(stderr, "path too long: %s/%s\n", path, name);
else
path[len] = '/';
strcpy(path + len + 1, name);
listdir(path, size);
path[len] = '\0';
else
printf("%s/%s\n", path, name);
closedir(dir);
int main(void)
char path[1024] = ".";
listdir(path, sizeof path);
return 0;
在我的系统上,它的输出与find .
的输出完全相同
【讨论】:
chqrlie 回答查理,这很混乱:p。 @Stargateur:是的,我也注意到了,但没有任何联系。 在 mac 上总是命中路径太长 @snr:您可能传递了一个不够长的数组,或者您的目录树中可能有一个循环。path too long:
后面显示的目录是什么?
@chqrlie 很有趣,它现在就像一个魅力。是DFS(深度优先搜索)方法吗?以上是关于如何在 Linux 上递归列出 C 中的目录?的主要内容,如果未能解决你的问题,请参考以下文章