gcc -o minitree minitree.cCOMP2113 的课程包含在 CS 服务器上进行操作。作为非管理员用户通过 ssh 登录到服务器上的时候,常因为没有一些称手的工具而感到不习惯。
其中之一就是 tree,也就是以树状图列出当前文件目录的工具。
于是打算用 C 实现一个。
dirent 库可以用来读取当前目录。借助了 AI 和 Manpage 之后写出了一个类似下面的基础版本:
#include <stdint.h> // uint8_t
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h> // check file type
#include <stdlib.h> // exit()
int has_space(char *s); // return 1 if s has space in it, else 0
void scan_dir(const char *path, const uint8_t depth) {
DIR *d;
struct dirent *dir;
struct stat st;
d = opendir(path);
if (d == NULL) {
perror("Could not open directory");
exit(EXIT_FAILURE);
}
while ((dir = readdir(d)) != NULL) {
char *name = dir->d_name;
stat(name, &st);
if (name[0] != '.') {
if (depth) printf("%*c", depth, ' ');
int need_quote = has_space(name);
if (need_quote) printf("'");
printf("%s", name);
if (S_ISDIR(st.st_mode)){
printf("/\n");
char buf[256];
sprintf(buf, "%s/%s", path, name);
scan_dir(buf, depth + 1);
}
if (need_quote) printf("'");
printf("\n");
}
}
closedir(d);
}DIR *opendir(char *path) 可以打开一个目录,然后用 struct dirent *readdir(DIR *d) 可以连续地从目录中读取一个条目。
stat 可以用来获取一个条目的属性。用 S_ISDIR() 来判断是否是目录。
这段代码有几个问题:
- 在使用
stat(char *path, struct stat *st)获取属性的时候没有传入完整路径,而stat只会在当前工作目录查找。故在递归进入内层目录的时候,如果尝试stat("file_in_subdir", &st),会导致ENOENT(Error NO ENTry)错误,从而让st_mode储存无意义值,导致无法正确判断是否为目录,并在下一次opendir()时报错。 printf("%*c", depth, ' ');语义不清,并且如果想调整缩进的长度,只能使用depth * LENGTH之类的方式。sprintf()没有显式地传入目标字符串的容量,如果格式化后的字符串超过buf的容量(256),会导致未知错误。- 对于一个目录条目,会在遍历完其内容后添加额外的换行(由于第一点,这个错误只能在子目录为空的时候被观测到)。
借助了 AI debug 和自己修改之后,写出了这样的版本:
#include <dirent.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
void scan_dir(const char *path, uint8_t depth) {
DIR *d = opendir(path);
struct dirent *dir;
struct stat st;
if (!d) {
perror("opendir");
return;
}
while ((dir = readdir(d)) != NULL) {
const char *name = dir->d_name;
if (name[0] == '.')
continue;
char fullpath[PATH_MAX];
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, name);
if (stat(fullpath, &st) != 0)
continue;
for (int i = 0; i < depth; i++)
putchar(' ');
int need_quote = has_space(name);
if (need_quote) putchar('\'');
printf("%s", name);
if (S_ISDIR(st.st_mode)) {
if (need_quote) putchar('\'');
printf("/\n");
scan_dir(fullpath, depth + 1);
} else {
if (need_quote) putchar('\'');
putchar('\n');
}
}
closedir(d);
}
修改后的代码不仅解决了上述的问题,还使用提前 return 和 continue 的方法避免了代码的 over-nesting。但是,仍然有一个问题需要解决:readdir() 是乱序的,并且无法预知最后一个条目的到来——对于一个功能完整的 tree,最后一个条目没有指向 next sibling 的分叉图示:
├── file1
├── file2
└── file3 // final entry
AI 给出的解决方案是提前存储当前列表的所有东西,并提前进行判断和排序。在本项目中我们只在意一个条目:
- 的名称;和
- 是否为目录。 所以我们可以写一个这样的结构体:
typedef struct {
char name[NAME_MAX + 1];
int is_dir;
} entry;然后实现两个函数:
void print_tree(const char *path, int *branches, const uint8_t depth);
size_t read_dir(const char *path, entry *entries, size_t max);print_tree() 会调用 read_dir(),后者先读取整个目录并整理,将排序后的结果放到 entries 中。如此一来,所有 dirent 和 stat 库的内容只会存在于 read_dir() 里,而 print_tree() 只需处理字符串输出。
void print_tree(const char *path, int *branches, const uint8_t depth) {
entry entries[256];
/* read_dir returns number of entries */
size_t n = read_dir(path, entries, 256);
for (size_t i = 0; i < n; i++) {
int last = (i == n - 1);
for (int d = 0; d < depth; d++)
printf(branches[d] ? "│ " : " ");
printf(last ? "└── " : "├── ");
int need_quote = has_space(entries[i].name);
if (need_quote) putchar('\'');
printf("%s", entries[i].name);
if (entries[i].is_dir) {
putchar('/');
if (need_quote) putchar('\'');
putchar('\n');
char next_path[PATH_MAX];
snprintf(next_path, PATH_MAX, "%s/%s", path, entries[i].name);
branches[depth] = !last;
print_tree(next_path, branches, depth + 1);
} else {
if (need_quote) putchar('\'');
putchar('\n');
}
}
}这个数组储存了到当前目录位置的各层目录是否是该层最后一个,也就直接决定了那一层是否需要输出一个枝干:
│ │ └── foo_dir
│ │ [NO] ├── foo_foo
│ │ [YES] ├── File1
│ │ │ ├── File2
│ │ │ ├── File3
│ [NO] │ │ └── File4
│ [YES] └── foo_bar_dir
│ │ ├── 'Hello World'
│ │ └── 'foo file bar'
│ └── file_in_parent_dir
在 read_dir() 函数递归调用的时候,会对 branches 进行可覆盖的修改并遗传给下一层调用。
size_t read_dir(const char *path, entry *entries, size_t max) {
DIR *d = opendir(path);
struct dirent *dir;
struct stat st;
size_t count = 0;
if (!d) return 0;
while ((dir = readdir(d)) && count < max) {
const char *name = dir->d_name;
if (name[0] == '.') continue; // TODO: control with flag?
char fullpath[PATH_MAX];
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, name);
if (lstat(fullpath, &st) != 0) continue;
snprintf(entries[count].name, sizeof(entries[count].name), "%s", name);
entries[count].is_dir = S_ISDIR(st.st_mode);
count ++;
}
closedir(d);
/* in-place sorting
* cmp_ent() is a function */
qsort(entries, count, sizeof(entries[0]), cmp_ent);
return count;
}void qsort (void *_LIBC_SIZE (__nel * __width) __base,
size_t __nel, size_t __width,
int (* _Nonnull __compar)(const void *, const void *));
qsort() 可以 in-place 地对一个数组进行排序,它需要:
__base:目标数组;__nel:数组的有效长度(元素数量);__width:一个数组元素的大小;和- 一个接受两个
const void *类型参数,返回int的函数,用作排序时的比较。
对于我们的例子,我们使用了这个比较函数:
int cmp_ent(const void *ea, const void *eb) {
const entry *entryA = (const entry *) ea;
const entry *entryB = (const entry *) eb;
// Prioritize folders
if (entryA->is_dir != entryB->is_dir) {
return entryB->is_dir - entryA->is_dir;
} else {
return strcmp(entryA->name, entryB->name);
}
}README.md: CC-BY-NC-SA 4.0
Other code: MIT