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

Posted

技术标签:

【中文标题】如何仅在 C 中列出第一级目录?【英文标题】:How to list first level directories only in C? 【发布时间】:2017-01-18 16:20:03 【问题描述】:

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

#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: */: 没有这样的文件或目录

【问题讨论】:

只需致电system。 Unix 上的 Glob 由 shell 扩展。 system会给你一个shell。 感谢@PSkocik,做到了!想发布答案吗? system("/bin/ls -d */"); 解释为什么 execv() 不能做到这一点;) 记住,如果你使用system(),你不应该也使用fork() 正确@unwind,我写了代码,main()的正文中的3行代码。 避免使用system(),并尽可能使用execv()system() 需要正确引用并且是许多安全问题的根源。您的问题是 '*' 是由 shell 扩展的,而不是由 ls 扩展的。您可以尝试执行find -type d 而不是。 【参考方案1】:

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

那么看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 中读取目录条目和打印文件信息的完全可编译示例。(不是 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_type,那会很糟糕。不便宜。 d_type 只是一个性能捷径;你总是需要一个后备(除了可能在为嵌入式系统编写时,你知道你正在使用什么 FS 并且它总是填写 d_type,并且你有一些方法可以在将来有人试图检测损坏时在另一种 FS 类型上使用此代码。)

【讨论】:

使用dirfd (3)fstatat (2),您可以使用任何目录。不仅是当前的。 @Igor 上面的代码提示你只能使用当前目录吗? @ChristopherSchultz:我使用了stat(de-&gt;d_name, &amp;stbuf);,即直接使用来自readdir 的目录条目作为相对路径,即相对于当前目录。使用 dirfd 和 fstatat 是一个很好的建议,可以相对于 另一个 目录使用它们,而不是进行字符串操作来创建指向该目录的路径。 @PeterCordes 啊,感谢您指出这一点。我假设字符串操作不是问题,@Igor 声称调用chdir 是使用stat 所必需的。【参考方案2】:

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

(在非共享文件系统上运行某些并行模拟时,在收集阶段,我偶尔会在同一目录中拥有数万个文件。是的,我可以以不同的方式执行此操作,但这恰好是最简单且收集数据的最可靠方法。实际上很少有 POSIX 实用程序愚蠢地假设“X 对每个人来说都足够了”。)

幸运的是,有几种解决方案。一种是改用find

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

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

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

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

system("/usr/bin/find . -mindepth 1 -maxdepth 1 -type d -printf '%p\0' | sort -t= | tr -s '\0' '\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() 的唯一“问题”是选择函数可能使用的大量文件描述符(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

【讨论】:

find -delete 对于这种特殊情况会更容易。但是xargs -0 就是一个很好的例子。对于 GNU find,find -exec rmdir + 会将 args 批处理成最大大小的组(与 find -exec rmdir \; 不同),因此它通常可以替换 xargs。 @PeterCordes:同意。我想知道是否要使用handle = popen("find ... -print0", "r");handle = popen("find ... -printf '%p\n'")getdelim(&amp;name, &amp;namesize, '\0', handle) 来查找特定文件,因为这是一种很好的KISS 方式(假设我们不关心用户是否对@ 做了一些奇怪的事情) 987654358@ 实用程序或PATH)。 编辑:当然是上面的handle = popen("find ... -printf '\p\0'"); 谨慎引用。 \0 在 C 双引号字符串文字内会提前终止它。我想你的意思是"... -printf '\\p\\0'" @PeterCordes: 啊啊啊啊。不,应该是handle = popen("find ... -printf '%p\\0'", "r");。无论如何,如果您在插件目录或子目录中允许插件或模板,则该方法特别好,并带有表示其类型的特定文件名后缀。非常人性化。在实践中,它最终看起来像handle = popen(FIND_CMD " " PLUGIN_DIRS " " FIND_PLUGIN_SPEC " -printf '%p\\0'", "r"); 或其他东西,宏在编译时确定(例如,有人想在他们的发行版上明确使用/usr/bin/find)。【参考方案3】:

只需致电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,您应该从一个进行正确错误检查的模板开始——每个调用都可能失败。)

【讨论】:

因为我刚刚开始使用它只提供一个目录,并且对发现* 的问题感到很困惑,你能不能用'通配符'替换'globs' - 并解释一下为什么这些是ls的问题? 真正低级的只有fd= opendir(".")readdir(fd)。在条目上使用stat(),如果 readdir 不返回文件类型信息,让您找到目录而不用说明ever dirent。 @RadLexus: ls 和其他普通的 Unix 程序不会将它们的 args 视为通配符。因此,在 shell 中,您可以运行 ls '*' 将文字 * 传递给 ls。使用strace ls * 查看运行时实际得到的args ls。某些从 DOS 移植的程序(或将 glob 用于特殊目的)将具有内置的 glob 处理功能,因此您必须使用额外的引号层来保护元字符免受 shell 的影响如果你想处理任意文件名,shell 也会从程序中传递它们。 添加了一个使用 POSIX opendir 和 d_type 的答案,并回退到 stat。我将把它留给其他人直接使用 Linux getdents() 系统调用编写答案。在这种特殊情况下使用glob 对我来说似乎很愚蠢。 @PSkocik:正如我所说,readdir() 在这种特殊情况下是可以的。在没有竞争的情况下避免文件描述符限制的唯一真正有效的方法是生成辅助从属进程以将早期的描述符保存在托管中。当换取可靠性时,速度无关!您可能会考虑快速但有时不正确的“技术上更好”,但我不这么认为。【参考方案4】:

如果您正在寻找一种将文件夹列表添加到程序中的简单方法,我宁愿建议使用无生成方式,而不是调用外部程序,并使用标准 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;

【讨论】:

使用d_type 而不检查DT_UNKNOWN 是错误的。你的程序永远不会在典型的 XFS 文件系统上找到目录,因为 mkfs.xfs 没有启用 -n ftype=1,所以文件系统不会廉价地提供文件类型信息,所以它设置 d_type=DT_UNKNOWN。 (当然还有任何其他总是有 DT_UNKNOWN 的 FS)。对于 DT_UNKNOWN 和符号链接(如果它们是指向目录的符号链接,也保留 */ 的那部分语义),请参阅我的答案。与往常一样,较低级别的高性能 API与更高级别的 API 相比,隐藏的复杂性更少。 @PeterCordes:我刚刚注意到您的更多更完整的答案! (我来这里是为了投票和咀嚼泡泡糖,但唉,我的票都没有了。) 我想你是在我开始研究我的工作后发布的,可能只是在我阅读完现有答案之后(两者都没有接近我所说的“低级”)。我的意思是,我的答案仍然不是使用直接系统调用的汇编语言,而是使用 glibc 函数调用,我什至使用了 printf! 很好的方法@RadLexus!【参考方案5】:

另一种较低级别的方法,system():

#include <stdlib.h>

int main(void)

    system("/bin/ls -d */");
    return 0;

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


正如Nomimal Animal所说,当子目录数量太大时,这将失败!请参阅他的答案以了解更多信息...

【讨论】:

如果目录包含如此多的子目录以致将它们全部列出将超过最大命令行长度,则此方法将不起作用。这会影响所有依赖 shell 进行 globbing 的答案,并将它们作为参数提供给像 ls 这样的单个命令。详情见我的回答。 感谢@NominalAnimal 让我知道。但是,我不会删除,因为它可以用于简单的使用。 :) 更新! :)

以上是关于如何仅在 C 中列出第一级目录?的主要内容,如果未能解决你的问题,请参考以下文章

dos命令怎么使用当前目录的上一级目录下的某个指定文件?

取消列出 R 中列表的最后一级

word 目录大纲第一级怎么居中对齐

老男孩教育-第2周课前测试考试题

如何过滤出已知当前目录下oldboy中的所有一级目录

对指定多个目录的第一级保留进行保留(再递归删除空目录)