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

在terminal中,我可以调用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; } 

这将是一切。 但是,当我尝试:

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

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

ls:* /:没有这样的文件或目录

不幸的是,基于shell扩展的所有解决方案都受到最大命令行长度的限制。 其中变化(运行true | xargs --show-limits出来); 在我的系统上,大概是两兆字节。 是的,很多人会认为这是足够的 – 就像比尔盖茨640千字节一样。

(当在非共享文件系统上运行某些并行模拟时,偶尔在收集阶段在同一个目录中有数以万计的文件。是的,我可以做到这一点,但是这是最简单,最稳健的方法收集数据,很少有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=可以用\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-*/删除rmdir directory-*/出于同样的原因。 使用

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

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

 cd .. rm -rf lots-of-subdirs 

最低级别的方法是使用ls使用的相同的Linux系统调用。

所以看看strace -efile,getdents 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的readdir(3) POSIX API函数的引擎下使用的。


最低级别的可移植方式(可移植到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)手册页中还有一个完全可编辑的读取目录条目和打印文件信息的stat(3posix) (不是Linux stat(2)手册页 ;它有一个不同的例子)。


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来说是一个巨大的提速:它不需要统计任何东西来通过subdirs递归。 但是对于不这样做的文件系统, d_type将始终是DT_UNKNOWN ,因为填充它将需要读取所有的inode(甚至可能不会从磁盘加载)。

有时候,你只是匹配文件名,不需要类型信息,所以如果内核花费了大量额外的CPU时间(尤其是I / O时间),当它不便宜的时候填充d_type会很糟糕。 d_type只是一个性能捷径; 你总是需要一个后备(除非可能在为嵌入式系统编写一个你知道你正在使用的FS并且总是填充d_type的嵌入式系统的d_type ,并且你有一些方法可以在将来某个人试图使用这个时候检测到破坏代码在另一个FS类型。)

只要打电话给system Unix上的Globes由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,你应该从模板开始,进行适当的错误检查 – 每个调用都可能失败。)

如果你正在寻找一个简单的方法来获取你的程序中的文件夹列表,我宁愿建议的方式,不调用外部程序,并使用标准的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; } 

另一个低层次的方法,用system() :

 #include <stdlib.h> int main(void) { system("/bin/ls -d */"); return 0; } 

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


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