如何仅在 C 中列出一级目录?
How to list first level directories only in C?
我可以在终端中调用 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: */: 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_type
。 d_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
我可以在终端中调用 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: */: 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_type
。 d_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