C 语言排序:根据条件选择排序依据

这是一个简单的 ls 实现,于2023年3月4日学习 C 语言时编写。82 行处使用了选择排序,但是排序条件会根据参数的设置(-t -r)来选择排序的依据:

  • !!(param & P_r):检查参数 param 是否设置了标志位 P_r-r 是逆序参数)。双感叹号 !! 用于将布尔值转换为 0 或 1。
  • 逻辑异或运算 ^:根据 P_r 设置情况,决定后续的排序判断结果是否需要翻转。
  • (param & P_t):检查参数 param 是否设置了标志位 P_t。如果设置了,则表示需要根据文件的修改时间进行排序,否则根据文件名进行排序。
1
2
3
4
5
6
7
8
9
10
11
12
// 排序文件列表
for (int i = 1, j, temp; i < entc; i++) {
temp = enti[i];
for (j = i;
// 排序的 condition 很长
j > 0 && !!(param & P_r) ^ (param & P_t
? entv[enti[j - 1]].mtime > entv[temp].mtime
: strcmp(entv[enti[j - 1]].name, entv[temp].name) > 0);
j--)
enti[j] = enti[j - 1];
enti[j] = temp;
}

这里的排序的条件比较复杂,但是有效简化了排序的逻辑,不需要再写很多 if 语句来判断排序条件。

zls 完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include <dirent.h>
#include <grp.h>
#include <linux/limits.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>

void list(char dir[]);
void listfile(struct stat* einfo, char fname[]);
void parseParam(char arg[]);
char* mod2str(int mod);
char* num2str(unsigned int num);
char* uname(gid_t uid);
char* gname(gid_t gid);

static unsigned param = 0;
#define P_a 0b1
#define P_l 0b10
#define P_R 0b100
#define P_t 0b1000
#define P_r 0b10000
#define P_i 0b100000
#define P_s 0b1000000

int main(int argc, char* argv[]) {
int havePath = 0;
for (int i = 1; i < argc; i++)
if (*argv[i] == '-')
parseParam(argv[i]);
else
havePath++;
while (--argc)
if (**++argv != '-')
list(*argv);
if (!havePath)
list(".");
return 0;
}

void list(char dir[]) {
// 打开目录
DIR* dirp = opendir(dir);
if (!dirp) {
closedir(dirp);
struct stat einfo;
if (stat(dir, &einfo) == -1)
fprintf(stderr, "zls: 无法访问 '%s': 没有那个文件或目录。\n", dir);
else if (!(param & P_R)) {
listfile(&einfo, dir);
printf("\n");
}
return;
}
printf("%s:\n", dir);
// 生成列表
int entc, enti[PATH_MAX];
struct entbrief {
char name[NAME_MAX];
time_t mtime;
};
struct entbrief* entv = malloc(sizeof(struct entbrief[PATH_MAX]));
struct dirent* entp;
for (entc = 0; (entp = readdir(dirp)) != NULL;) {
if (!(param & P_a) && *entp->d_name == '.')
continue;
struct stat einfo;
strcpy(entv[entc].name, entp->d_name);
if (param & P_t) {
char path[PATH_MAX];
sprintf(path, "%s/%s", dir, entp->d_name);
if (stat(path, &einfo) == -1)
continue;
entv[entc].mtime = einfo.st_mtim.tv_sec;
}
enti[entc] = entc;
entc++;
}
// 排序
for (int i = 1, j, temp; i < entc; i++) {
temp = enti[i];
for (j = i;
// 排序的condition比较长,所以称之为“悲伤的猪大排”
j > 0 &&
!!(param & P_r) ^
(param & P_t
? entv[enti[j - 1]].mtime > entv[temp].mtime
: strcmp(entv[enti[j - 1]].name, entv[temp].name) > 0);
j--)
enti[j] = enti[j - 1];
enti[j] = temp;
}
// 输出
for (int i = 0; i < entc; i++) {
struct stat einfo;
char path[PATH_MAX];
sprintf(path, "%s/%s", dir, entv[enti[i]].name);
if (stat(path, &einfo) == -1)
continue;
listfile(&einfo, entv[enti[i]].name);
printf(param & P_l || i % 5 == 4 ? "\n" : " \t");
}
printf(param & P_l ? "\n" : "\n\n");
closedir(dirp);
// 递归
for (int i = 0; i < entc && param & P_R; i++) {
if (!strcmp(entv[i].name, ".") || !strcmp(entv[i].name, ".."))
continue;
char path[PATH_MAX];
sprintf(path, "%s/%s", dir, entv[i].name);
list(path);
}
free(entv);
}

void listfile(struct stat* einfo, char fname[]) {
if (param & P_i)
printf("%8lu ", einfo->st_ino);
if (param & P_s)
// Zhilu 纠正:应该是4KB向上取整
printf("%4ld ",
(einfo->st_size / 4096 * 4) + (einfo->st_size % 4096 ? 4 : 0));
if (param & P_l) {
printf("%s ", mod2str(einfo->st_mode));
printf("%4d ", (int)einfo->st_nlink);
printf("%-8s ", uname(einfo->st_uid));
printf("%-8s ", gname(einfo->st_gid));
printf("%8ld ", einfo->st_size);
printf("%.12s ", 4 + ctime((const time_t*)&einfo->st_mtim));
}
printf(S_ISDIR(einfo->st_mode) ? "\033[1;34m%-10s\033[0m" : "%-10s", fname);
}

void parseParam(char arg[]) {
#define CheckParam(ch) \
if (*arg == *#ch) { \
param |= P_##ch; \
continue; \
}
while (*++arg) {
CheckParam(a);
CheckParam(l);
CheckParam(R);
CheckParam(t);
CheckParam(r);
CheckParam(i);
CheckParam(s);
fprintf(stderr, "zls: 未知参数 '-%c'。\n", *arg);
}
#undef CheckParam
}

char* mod2str(int mod) {
static char str[] = "----------";
str[0] = S_ISDIR(mod) ? 'd' : S_ISLNK(mod) ? 'l' : '-';
str[1] = mod & S_IRUSR ? 'r' : '-';
str[2] = mod & S_IWUSR ? 'w' : '-';
str[3] = mod & S_IXUSR ? 'x' : '-';
str[4] = mod & S_IRGRP ? 'r' : '-';
str[5] = mod & S_IWGRP ? 'w' : '-';
str[6] = mod & S_IXGRP ? 'x' : '-';
str[7] = mod & S_IROTH ? 'r' : '-';
str[8] = mod & S_IWOTH ? 'w' : '-';
str[9] = mod & S_IXOTH ? 'x' : '-';
return str;
}

char* num2str(unsigned int num) {
static char str[NAME_MAX];
sprintf(str, "%d", num);
return str;
}

char* uname(gid_t uid) {
struct passwd* pwp = getpwuid(uid);
return pwp ? pwp->pw_name : num2str(uid);
}

char* gname(gid_t gid) {
struct group* grp = getgrgid(gid);
return grp ? grp->gr_name : num2str(gid);
}

程序中关于参数解析的内容也比较有意思,使用了宏函数来解析参数,同时二进制标志位来存储参数。

HTML 模板生成器

2023年2月7日,我建立了一个简单的网址导航页面。4 天后,我和半岛的孤城聊了聊,他觉得用 HTML 手搓导航项不够优雅,于是推荐我用类似 JSON 的结构来维护导航项。

他推荐我用 JS 的模板字符串来生成导航,但是遇到了一些问题,最后发现是生成的 HTML 标签没有正确闭合,导致浏览器报错。同时,由于我的导航项比较复杂,所以写出的模板生成器代码也很抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let nav = {
name: "CO导航",
description: "",
list: [],
ele: document.getElementsByClassName("navlist"),
}

nav.list[0] = [{
name: "线上课时", icon: "fa-solid fa-chalkboard-user", item: [
{ text: "学习通", icon: "iconfont icon-chaoxing", link: "http://i.chaoxing.com/" },
]
}, {
name: "西邮生活", icon: "fa-solid fa-school", item: [
{ text: "我在校园", desc: "网页版登录", icon: "fa-solid fa-location-dot", js: "dialog.showMsg(this.textContent)" },
]
},]

nav.list.forEach((list, i) => {
nav.ele[i].innerHTML = list.map(group => `
<div class="card">
<div class="between">
<h4><i class="${group.icon} fa-space"></i>${group.name}</h4>
${group.desc ? `<p class="dim">${group.desc}</p>` : ``}
</div>
<div class="list">
${group.item.map(item => `
<a data-sub="${item.desc || ""}"
${item.js ? `onclick="${item.js}"` : `href="${item.link}"`}
>${item.icon ? `<i class="${item.icon} fa-space"></i>` : ``}${item.text}</a>
`).join(`\n`)}
</div>
</div>`).join(`\n`)
})

模板生成器的嵌套结构比较复杂:

  • 使用 nav.list.forEach() 遍历名为 nav.list 里的每一个导航块。
  • 在每个导航块中,使用 map() 方法创建新的字符串数组,使用 join() 方法将数组里的导航组拼接为字符串。
  • 在每个导航组中,使用 map() 方法创建新的字符串数组,使用 join() 方法将数组里的导航项拼接为字符串。
    • 导航项可能会有图标、小字描述。
    • 有的导航项不是链接,而是 JS 脚本。

其实也可以用 createElement()appendChild() 来生成 HTML,或者 Vue 的组件也很方便。不过,用原生 JavaScript 写出来了这些,感觉还是挺奇妙的。

面向函数而不是面向过程

2024年4月6日,我在维护一个网页工具的背景列表。由于这个项目已经运营了三年,背景列表已经有了四百多张图片,并且还携带了一些描述信息。为了让代码看起来短一些,我先使用比较抽象的数据结构来存储,在使用时再生成更友好的数据结构。

1
2
3
4
const gallery = {
End22: [["呱唧", "山望", "2/12/16/HBqoL"], ["呱唧", "瞰林", "2/12/16/HBBGX"],],
Jan23: [["酸子", "云中印象", "3/01/03/E8Lwa"], ["酸子", "镜中暮", "3/01/03/E8HSK"],],
}

经过某个处理函数后,它应当变成这种结构:

1
2
3
4
5
6
const galleryFlated = [
{vol: "End22", author: "呱唧", name: "山望", url: "https://ooo.0x0.ooo/2022/12/16/HBqoL.jpg"},
{vol: "End22", author: "呱唧", name: "瞰林", url: "https://ooo.0x0.ooo/2022/12/16/HBBGX.jpg"},
{vol: "Jan23", author: "酸子", name: "云中印象", url: "https://ooo.0x0.ooo/2023/01/03/E8Lwa.jpg"},
{vol: "Jan23", author: "酸子", name: "镜中暮", url: "https://ooo.0x0.ooo/2023/01/03/E8HSK.jpg"}
]

刚开始使用面向过程的方式来处理,后来发现,使用面向函数的方式更简短清晰。

1
2
3
4
const galleryFlated = Object.entries(gallery).flatMap(([vol, picInfos]) =>
picInfos.map(([author, name, shortURL]) => ({
vol, author, name, url: `https://ooo.0x0.ooo/202${shortURL}.jpg`,
})))

总结

这些代码不算是 Best Practice,甚至有些抽象,不过挺有意思的。