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

时间:2016-09-10 19:24:43

标签: c linux fork low-level systems-programming

在终端中,我可以拨打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:* /:没有这样的文件或目录

5 个答案:

答案 0 :(得分:10)

执行此操作的最低级别方法是使用相同的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中读取目录条目和打印文件信息。(不是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类型上使用此代码。)

答案 1 :(得分:4)

只需致电system即可。 Unix上的Globs由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,你应该从一个执行正确错误检查的模板开始 - 每个调用都可能失败。)

答案 2 :(得分:4)

如果您正在寻找一种简单的方法来获取程序中的文件夹列表,我宁愿建议使用spawnless方式,而不是调用外部程序,并使用标准POSIX opendir / {{1函数。

几乎与您的程序一样短,但还有其他一些优势:

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

.

答案 3 :(得分:4)

不幸的是,所有基于shell扩展的解决方案都受到最大命令行长度的限制。其中有所不同(运行true | xargs --show-limits以查明);在我的系统上,它大约是2兆字节。是的,许多人认为这就足够了 - 比尔盖茨就像640千字节一样。

(在非共享文件系统上运行某些并行模拟时,在收集阶段,我偶尔会在同一目录中拥有数万个文件。是的,我可以这样做,但这恰好是最简单的收集数据的最有力的方法。很少有POSIX实用程序实际上足以假设&#34; 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()时唯一的&#34;问题&#34; 是选择函数可能使用的大量文件描述符(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

答案 4 :(得分:2)

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

#include <stdlib.h>

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

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

正如Nomimal Animal所说,当子目录的数量太大时,这将失败!看到他的答案更多......