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。如果设置了,则表示需要根据文件的修改时间进行排序,否则根据文件名进行排序。
// 排序文件列表
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 完整代码
#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 标签没有正确闭合,导致浏览器报错。同时,由于我的导航项比较复杂,所以写出的模板生成器代码也很抽象。

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

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

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

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"}
]

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

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,甚至有些抽象,不过挺有意思的。