如何仅在 C 中列出一级目录?

How to list first level directories only in C?

我可以在终端中调用 ls -d */。现在我想要一个 程序来为我做这件事,像这样:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>

int main( void )
{
    int status;

    char *args[] = { "/bin/ls", "-l", NULL };

    if ( fork() == 0 )
        execv( args[0], args );
    else
        wait( &status ); 

    return 0;
}

这将 ls -l 一切。但是,当我尝试时:

char *args[] = { "/bin/ls", "-d", "*/",  NULL };

我会得到一个运行时错误:

ls: */: No such file or directory

只需拨打system。 Unix 上的 Glob 由 shell 扩展。 system 会给你一个 shell.

你可以自己做 glob(3) 来避免整个 fork-exec 的事情:

int ec;
glob_t gbuf;
if(0==(ec=glob("*/", 0, NULL, &gbuf))){
    char **p = gbuf.gl_pathv;
    if(p){
        while(*p)
            printf("%s\n", *p++);
    }
}else{
   /*handle glob error*/ 
}

您可以将结果传递给生成的 ls,但这样做几乎没有意义。

(如果您确实想要执行 fork 和 exec,您应该从一个进行适当错误检查的模板开始——每个调用都可能失败。)

另一种不太低级的方法,system():

#include <stdlib.h>

int main(void)
{
    system("/bin/ls -d */");
    return 0;
}

注意 system(),您不需要 fork()。但是,我记得我们应该尽可能避免使用 system()


正如Nomimal Animal所说,当子目录数量过多时,这将失败!查看他的回答了解更多...

如果您正在寻找一种将文件夹列表放入您的程序的简单方法,我宁愿建议使用无生成方式,而不是调用外部程序,并使用标准 POSIX opendir/readdir 函数。

几乎和您的程序一样短,但还有几个额外的优点:

  • 您可以通过勾选 d_type
  • 来随意选择文件夹和文件
  • 您可以选择通过测试 .
  • 名称的第一个字符来提前丢弃系统条目和(半)隐藏条目
  • 您可以立即打印出结果,或者将其存储在内存中以备后用
  • 您可以对内存中的列表进行额外的操作,例如排序和删除不需要包含的其他条目。

#include <stdio.h>
#include <sys/types.h>
#include <sys/dir.h>

int main( void )
{
    DIR *dirp;
    struct dirent *dp;

    dirp = opendir(".");
    while ((dp = readdir(dirp)) != NULL)
    {
        if (dp->d_type & DT_DIR)
        {
            /* exclude common system entries and (semi)hidden names */
            if (dp->d_name[0] != '.')
                printf ("%s\n", dp->d_name);
        }
    }
    closedir(dirp);

    return 0;
}

执行此操作的最低级别方法是使用相同的 Linux 系统调用 ls

所以看看strace -efile,getdents ls的输出:

execve("/bin/ls", ["ls"], [/* 72 vars */]) = 0
...
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 23 entries */, 32768)    = 840
getdents(3, /* 0 entries */, 32768)     = 0
...

getdents 是一个 Linux 特定的系统调用。手册页说它被 libc's readdir(3) POSIX API function.

在幕后使用

最底层的可移植方式(可移植到POSIX系统),就是使用libc函数打开一个目录,读取条目。 POSIX 没有指定确切的系统调用接口,这与非目录文件不同。

这些函数:

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);

可以这样使用:

// print all directories, and symlinks to directories, in the CWD.
// like sh -c 'ls -1UF -d */'  (single-column output, no sorting, append a / to dir names)
// tested and works on Linux, with / without working d_type

#define _GNU_SOURCE    // includes _BSD_SOURCE for DT_UNKNOWN etc.
#include <dirent.h>
#include <stdint.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    DIR *dirhandle = opendir(".");     // POSIX doesn't require this to be a plain file descriptor.  Linux uses open(".", O_DIRECTORY); to implement this
    //^Todo: error check
    struct dirent *de;
    while(de = readdir(dirhandle)) { // NULL means end of directory
        _Bool is_dir;
    #ifdef _DIRENT_HAVE_D_TYPE
        if (de->d_type != DT_UNKNOWN && de->d_type != DT_LNK) {
           // don't have to stat if we have d_type info, unless it's a symlink (since we stat, not lstat)
           is_dir = (de->d_type == DT_DIR);
        } else
    #endif
        {  // the only method if d_type isn't available,
           // otherwise this is a fallback for FSes where the kernel leaves it DT_UNKNOWN.
           struct stat stbuf;
           // stat follows symlinks, lstat doesn't.
           stat(de->d_name, &stbuf);              // TODO: error check
           is_dir = S_ISDIR(stbuf.st_mode);
        }

        if (is_dir) {
           printf("%s/\n", de->d_name);
        }
    }
}

在 Linux stat(3posix) man page. (not the Linux stat(2) man page 中还有一个完全可编译的读取目录条目和打印文件信息的示例;它有一个不同的例子。


readdir(3) 的手册页说 struct dirent 的 Linux 声明是:

   struct dirent {
       ino_t          d_ino;       /* inode number */
       off_t          d_off;       /* not an offset; see NOTES */
       unsigned short d_reclen;    /* length of this record */
       unsigned char  d_type;      /* type of file; not supported
                                      by all filesystem types */
       char           d_name[256]; /* filename */
   };

d_type 要么是 DT_UNKNOWN,在这种情况下,您需要 stat 来了解目录条目本身是否是一个目录。或者它可以是 DT_DIR 或其他东西,在这种情况下,您可以确定它是或不是目录,而不必 stat 它。

一些文件系统,比如我认为的 EXT4 和最近的 XFS(具有新的元数据版本),将类型信息保存在目录中,因此无需从磁盘加载 inode 即可返回。这对 find -name 来说是一个巨大的加速:它不需要统计任何东西来递归子目录。但是对于不这样做的文件系统,d_type 将始终是 DT_UNKNOWN,因为填充它需要读取所有 inode(甚至可能不会从磁盘加载)。

有时你只是匹配文件名,不需要类型信息,所以如果内核花费大量额外的 CPU 时间(或者特别是 I/O 时间,那就太糟糕了) 不便宜的时候填d_typed_type 只是一个性能捷径;你总是需要一个回退(除了可能在为嵌入式系统编写时你知道你正在使用什么 FS 并且它总是填写 d_type,并且你有一些方法来检测破损当有人在未来试图在另一种 FS 类型上使用此代码。)

不幸的是,所有基于shell扩展的解决方案都受到最大命令行长度的限制。哪个不同(运行 true | xargs --show-limits 找出);在我的系统上,它大约有两兆字节。是的,很多人会争辩说它就足够了——就像比尔盖茨曾经在 640 KB 上所做的那样。

(当 运行 在非共享文件系统上进行某些并行模拟时,在收集阶段,我偶尔会在同一目录中拥有数万个文件。是的,我可以做不同的事情,但是这恰好是收集数据的最简单和最可靠的方法。很少 POSIX 实用程序实际上愚蠢到假设 "X is sufficient for everybody"。)

幸运的是,有几种解决方案。一种是使用 find 代替:

system("/usr/bin/find . -mindepth 1 -maxdepth 1 -type d");

您也可以根据需要格式化输出,不依赖于语言环境:

system("/usr/bin/find . -mindepth 1 -maxdepth 1 -type d -printf '%p\n'");

如果要对输出进行排序,请使用 [=21=] 作为分隔符(因为文件名允许包含换行符),对于 sort 使用 -t= 使用 [=21= ] 作为分隔符。 tr 将为您将它们转换为换行符:

system("/usr/bin/find . -mindepth 1 -maxdepth 1 -type d -printf '%p[=12=]' | sort -t= | tr -s '[=12=]' '\n'");

如果您想要数组中的名称,请改用 glob() 函数。

最后,因为我喜欢不时地竖起大拇指,可以使用 POSIX nftw() 函数在内部实现:

#define _GNU_SOURCE
#include <stdio.h>
#include <ftw.h>

#define NUM_FDS 17

int myfunc(const char *path,
           const struct stat *fileinfo,
           int typeflag,
           struct FTW *ftwinfo)
{
    const char *file = path + ftwinfo->base;
    const int depth = ftwinfo->level;

    /* We are only interested in first-level directories.
       Note that depth==0 is the directory itself specified as a parameter.
    */
    if (depth != 1 || (typeflag != FTW_D && typeflag != FTW_DNR))
        return 0;

    /* Don't list names starting with a . */
    if (file[0] != '.')
        printf("%s/\n", path);

    /* Do not recurse. */
    return FTW_SKIP_SUBTREE;
}

和使用上面的 nftw() 调用显然类似于

if (nftw(".", myfunc, NUM_FDS, FTW_ACTIONRETVAL)) {
    /* An error occurred. */
}

使用 nftw() 的唯一 "issue" 是选择函数可能使用的大量文件描述符 (NUM_FDS)。 POSIX 表示一个进程必须始终能够拥有至少 20 个打开的文件描述符。如果我们减去标准的(输入、输出和错误),剩下 17。不过,上面不太可能使用超过 3。

您可以使用 sysconf(_SC_OPEN_MAX) 并减去您的进程可以同时使用的描述符数来找到实际限制。在当前 Linux 系统中,每个进程通常限制为 1024。

好处是,只要这个数字至少是 4 或 5 左右,它只会影响性能:它只是决定 nftw() 在目录树结构中可以进入多深,在它之前必须使用变通办法。

如果你想创建一个包含很多子目录的测试目录,使用类似下面的东西 Bash:

mkdir lots-of-subdirs
cd lots-of-subdirs
for ((i=0; i<100000; i++)); do mkdir directory-$i-has-a-long-name-since-command-line-length-is-limited ; done

在我的系统上,运行宁

ls -d */

在该目录中产生 bash: /bin/ls: Argument list too long 错误,而 find 命令和基于 nftw() 的程序都 运行 正常。

出于同样的原因,您也无法使用 rmdir directory-*/ 删除目录。使用

find . -name 'directory-*' -type d -print0 | xargs -r0 rmdir

代替。或者只是删除整个目录和子目录,

cd ..
rm -rf lots-of-subdirs