手写Shell解释器:从fork、exec到管道与重定向的实现
动手写一个自己的 Shell 解释器这事听起来像是计算机课程里的作业但我是在工作两年后主动回头补的这门课。那时候整天在 Linux 服务器上敲命令用管道把 ps、grep、awk 串起来却说不清按下回车之后那几毫秒里到底发生了什么。直到自己动手实现了一个能跑简单命令、支持重定向和管道的小型 Shell才真正把 fork、exec、文件描述符这些概念串成了一条线。这篇文章就完整记录一下整个项目的过程从设计思路到核心代码再到调试时踩过的坑希望能帮你少走一些弯路。这个项目本身不复杂一个能读取用户输入、解析命令、执行程序、支持重定向和管道的最小 Shell。它适合三类人——刚学完 C 语言想找个综合项目练手的人、想彻底搞懂 Shell 工作原理的人、以及被各种脚本坑到怀疑人生想从底层理解“为什么”的人。做完这个项目你会发现那些 shell 脚本里诡异的语法、奇怪的坑根源全在解释器的工作方式上。1. 项目整体设计与思路拆解1.1 为什么用 C 语言而不是 Python做这个项目第一件事是选语言。我见过有人用 Python 写“shell”本质就是subprocess.run(input.split())几行代码完事。但这完全达不到目的——因为你被 Python 封装掉的东西恰恰是 Shell 的核心机制进程创建、程序替换、文件描述符重定向。用 C 语言的理由很直接fork()、exec()、dup2()、pipe()这些系统调用本身就是 C 语言的接口你在 shell 里看到的每一个行为——ls、cd、cat file | grep foo——最终都是这些系统调用的组合。用 C 写一遍你才能真正理解“Shell 只是个命令解释器”这句话的含义。还有个实际好处C 没有垃圾回收内存管理得自己做这逼着你去处理指针和缓冲区而这些恰恰是写解释器时最容易出错的地方。写完这个项目你的 C 指针水平会有明显提升。1.2 整体架构主循环的经典三段式所有 Shell 的内核都是同一个循环我把它叫作“读取-解析-执行”三段式读取Read打印提示符等待用户输入一行命令解析Parse把这一行字符串拆分成命令名、参数列表识别特殊符号管道|、重定向、等执行Execute根据解析结果决定是调用内建命令、还是创建子进程执行外部程序代码结构就是围绕这个循环展开的int main(void) { char *line NULL; ssize_t len 0; while (1) { printf(mysh ); fflush(stdout); if (getline(line, len, stdin) -1) { break; // Ctrl-D 退出 } cmdline *cmd parse_line(line); // 解析 if (cmd) { execute_cmd(cmd); // 执行 free_cmd(cmd); } } free(line); return 0; }你没看错一个 Shell 的主循环就这么多。但每个环节展开都有很多细节下面逐一拆解。1.3 这个设计背后规避了哪些问题我一开始图省事打算用system()函数直接执行命令。system(ls -l)一行就完事比 forkexec 简单太多了。但后来发现两个致命问题第一system()会再启动一个/bin/sh -c来执行命令等于在“你的 shell”和“系统 shell”之间多了一层进程这层进程会带来额外的环境变量污染和信号处理差异。你写的解释器不解释命令反而依赖另一个解释器这在逻辑上就说不通。第二system()内部用的是wait()而不是waitpid()如果你要同时管理多个子进程wait()经常会“抢走”不属于当前命令的子进程状态导致混乱。自己用 forkexec 写每个子进程的回收都清清楚楚。2. 核心细节解析与实操要点2.1 命令解析从一行字符串到 argv 数组解析器要做的事情是把ls -l -a /home/user这样的输入拆成一个结构体typedef struct { char *cmd; // 命令名 char **args; // 参数数组以 NULL 结尾 char *in_file; // 重定向输入文件 char *out_file; // 重定向输出文件 } cmdline;这一步看着简单但实际上有一个经典的大坑空格拆分。如果只是strtok(line, )然后逐个复制你会立刻发现带引号的参数被拆碎了比如echo hello world会变成三份echo、hello、world。我在第一版就踩了这个坑后来老老实实写了一个手动的状态机扫描器int split_tokens(char *line, char **tokens, int max_tokens) { int count 0; char *p line; while (*p count max_tokens - 1) { // 跳过前导空白 while (*p || *p \t) p; if (*p \0) break; if (*p || *p \) { // 处理引号包裹的参数 char quote *p; char *start p; while (*p *p ! quote) p; tokens[count] strndup(start, p - start); if (*p) p; // 跳过闭合引号 } else { // 普通参数读到空白为止 char *start p; while (*p *p ! *p ! \t) p; tokens[count] strndup(start, p - start); } } tokens[count] NULL; return count; }这段代码的核心思想是遇到引号就进入“引号模式”直到遇到下一个引号才结束否则按空白分隔。写完这个函数你就能体会到C 语言里“删除字符串首尾空格”“按引号分组”这些小操作是真的需要动手写才知道细节在哪。2.2 引号处理和转义符一个长期存在的痛点如果你读过一些 shell 脚本的入门教程肯定会碰到“引号嵌套”“转义符失效”这类问题。自己做解释器后你会发现这些问题的根源在于解释器是逐字符扫描的引号只是一个状态标记不是数据的一部分。比如echo Hello WorldShell 真正传给 echo 的参数是Hello World不是Hello World。同理echo He said \Hi\里的\表示一个字面量双引号不应该被当作引号处理。所以完整的解析器还要处理反斜杠转义。这里我给一个建议不要一上来就全支持先把基本的引号解析做对再加转义支持。我自己的实现里转义处理是在扫描阶段就完成的——遇到\且后面还有字符时直接输出后面的字符并跳过。2.3 缓冲区与内存管理解析器最怕的就是缓冲区溢出。早期代码喜欢用固定数组char buf[1024]一旦用户输入超长就崩。我在项目里强制自己使用getline()它会自动扩展缓冲区和strdup()自动分配精确大小的内存然后统一用free()释放。这一块的教训是每个 strdup 都对应一个 free做好配对。我在parse_line()里分配的所有字符串都会在execute_cmd主流程结束后统一释放。养成这个习惯后项目从头到尾没出现一次内存泄漏用 valgrind 检查的。3. 实操过程与核心环节实现3.1 内建命令cd、exit、pwd、export先区分一个概念内建命令builtin和外部命令external。内建命令是 Shell 自身实现的不需要启动新进程外部命令是系统里的可执行文件比如/bin/ls。为什么要区分看cd就明白了。cd要改变当前工作目录这个“当前工作目录”是每个进程的属性。如果你用 fork 创建子进程执行cd那只是改变了子进程的目录父进程Shell 本身的目录没变。下次执行命令还是在老地方。所以cd必须由 Shell 自己执行。我的内建命令用了一个简单的查表函数指针的架构int mysh_cd(char **args) { if (!args[1]) { // 没参数就回到 HOME chdir(getenv(HOME)); } else { if (chdir(args[1]) ! 0) { perror(mysh: cd); } } return 1; } int mysh_exit(char **args) { exit(0); return 1; } int mysh_pwd(char **args) { char buf[4096]; if (getcwd(buf, sizeof(buf))) { printf(%s\n, buf); } return 1; } struct builtin_t { char *name; int (*func)(char **); } builtins[] { {cd, mysh_cd}, {exit, mysh_exit}, {pwd, mysh_pwd}, {export, mysh_export}, {NULL, NULL} }; int is_builtin(char *name) { for (int i 0; builtins[i].name; i) { if (strcmp(name, builtins[i].name) 0) return 1; } return 0; }每次解析完命令先检查是不是内建命令是就直接调用对应函数不是才走 forkexec。3.2 外部命令fork exec 的工作原理外部命令的执行是 Shell 的“重头戏”核心逻辑只有三步pid_t pid fork(); if (pid 0) { // 子进程 execvp(cmd-args[0], cmd-args); // 如果 execvp 返回说明执行失败了 fprintf(stderr, mysh: command not found: %s\n, cmd-args[0]); exit(127); } else if (pid 0) { // 父进程等待子进程结束 int status; waitpid(pid, status, 0); } else { perror(fork); }这里最关键的一行是execvp(cmd-args[0], cmd-args)。exec系列函数的作用是用一个新的程序替换当前进程的代码段和数据段。execvp中的p表示会去PATH环境变量里找命令所以你不需要写/bin/ls直接写ls就行。而fork()做的事情是把当前进程完整复制一份包括内存、文件描述符、环境变量。子进程继承父进程的一切然后立刻被execvp替换成新程序。这就是 Unix 下创建新进程的标准姿势先 fork 复制自己再 exec 变成别人。3.3 重定向dup2 的魔法重定向是 Shell 最迷人的特性之一。ls output.txt做了两件事打开或创建output.txt然后把标准输出从屏幕改到这个文件。在 C 里用dup2()实现// 在子进程里执行 exec 之前判断 if (cmd-out_file) { int fd open(cmd-out_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd 0) { perror(open); exit(1); } dup2(fd, STDOUT_FILENO); // 把 fd 复制到标准输出1号fd close(fd); } if (cmd-in_file) { int fd open(cmd-in_file, O_RDONLY); if (fd 0) { perror(open); exit(1); } dup2(fd, STDIN_FILENO); // 把 fd 复制到标准输入0号fd close(fd); }这里的关键点是dup2(fd, STDOUT_FILENO)执行后文件描述符 1标准输出指向的内容变成了fd指向的那个文件。之后程序无论往标准输出写什么都会写到文件里。注意这些重定向操作必须在 fork 之后、exec 之前进行。因为重定向只对将要执行的那个命令生效不能影响 Shell 自己的标准输出。fork 出来的子进程天然继承了父进程的文件描述符所以在这个时间点上做重定向是干净利落的。3.4 管道把两个进程“接”起来管道实现牵扯到多进程协作是整个项目里最绕的部分。先看代码int pipefd[2]; pipe(pipefd); // 创建管道pipefd[0] 用于读pipefd[1] 用于写 pid_t pid1 fork(); if (pid1 0) { // 左侧命令例如 cat file // 把标准输出接到管道写端 close(pipefd[0]); // 关掉读端 dup2(pipefd[1], STDOUT_FILENO); // 标准输出 - 管道写端 close(pipefd[1]); execvp(left-args[0], left-args); exit(127); } pid_t pid2 fork(); if (pid2 0) { // 右侧命令例如 grep foo // 把标准输入接到管道读端 close(pipefd[1]); // 关掉写端 dup2(pipefd[0], STDIN_FILENO); // 标准输入 - 管道读端 close(pipefd[0]); execvp(right-args[0], right-args); exit(127); } // 父进程关闭两端等待两个子进程 close(pipefd[0]); close(pipefd[1]); waitpid(pid1, NULL, 0); waitpid(pid2, NULL, 0);管道的本质是pipe()创建一对文件描述符一个只能写一个只能读。数据从写端进入从读端读出内核自动缓冲。用管道连接两个命令就是把左侧命令的标准输出接到写端把右侧命令的标准输入接到读端。这里有一个必须理解的重点父进程必须关闭 pipefd 的两端。如果不关父进程仍然持有管道的写端文件描述符那么当左侧命令执行完退出、关闭它的写端后管道仍然没有断开因为父进程还握着写端右侧命令在读取时会一直阻塞等待数据永远读不到 EOF。这是一个经典的管道 bug我就是因为这个浪费了一个下午。3.5 混合场景先重定向再放进管道此刻你可能已经想到了cat file | grep foo out.txt这种既有管道又有重定向的命令怎么办答案很简单先处理管道在 fork 出的子进程里处理重定向。顺序是Shell 解析出左侧命令和右侧命令左侧子进程先让 stdout 指向管道写端再处理自己的重定向右侧子进程先让 stdin 指向管道读端再处理自己的重定向执行顺序的规则是管道接线在重定向之前。因为重定向和管道都通过 dup2 修改文件描述符只要 dup2 的目标位置不冲突先后顺序是可以叠加的。比如右侧命令的 stdout 被重定向到 out.txt同时它的 stdin 从管道读端获取数据这两个fd 0 和 fd 1互不干扰。4. 常见问题与排查技巧实录4.1 核心 Bug 记录表我在开发过程中记录了一张问题表列几个最有代表性的问题现象原因解决方案命令提示符不显示循环卡在 getline 等待输入没有输出mysh忘了调用fflush(stdout)printf 的缓冲区没有刷新在 printf 后加fflush(stdout)或用write(1, , 2)直接写管道命令卡死lsgrep c 执行后 ls 正常退出grep 一直等待父进程没关闭管道写端EOF 信号永远不来cd 后目录没变执行cd /tmp后pwd仍在原目录cd 通过 fork 在子进程里执行子进程改的是自己的目录cd 必须走内建命令由 Shell 进程亲自调用 chdir退出状态码不对外部命令失败返回 0fork 子进程里 exec 失败后没 exit导致子进程继续跑父进程的代码exec 失败后立刻exit(127)模仿 shell 的 “command not found” 状态码内存泄漏valgrind 检测出泄漏解析器里用 strdup 分了内存执行完没 free在命令执行完成后统一释放 cmd 结构体里的所有字符串指针4.2 僵尸进程和信号处理如果你不做信号处理跑一段时间后输入ps -ef会发现一堆defunct僵尸进程。原因是子进程退出后其退出状态码需要父进程用wait()回收。如果父进程一直不 wait子进程的状态就残留下来就成了僵尸进程。我的做法是在主循环里对每个 fork 出的子进程立即waitpid(pid, status, 0)。但如果你将来要写支持后台任务和任务控制的 Shell就不能用同步 wait 了得用信号机制——signal(SIGCHLD, handler)配合waitpid(-1, status, WNOHANG)子进程退出时内核给父进程发 SIGCHLD 信号父进程在信号处理函数里批量回收子进程。这一步做完Shell 才具备“同时活跃多个子进程不产生僵尸”的基本能力。这个项目里我没做后台任务但提前把信号处理函数挂上了代码很简单void sigchld_handler(int sig) { int status; while (waitpid(-1, status, WNOHANG) 0); } signal(SIGCHLD, sigchld_handler);4.3 Shell 脚本的坑在解释器这里找到了答案做完这个项目我终于理解为什么网上搜“shell 脚本入门”会出现那么多“常见坑”的帖子。比如for i in $(seq 1 10); do echo $i; done里的空格依赖 分词规则if [ $a -eq 1 ]里的括号两边必须有空格不然[后面的$a会粘在括号上echo hello output.txt为什么经常在文件内容里多一个空格这些坑本质上都不是“脚本语言语法”问题而是解释器处理文本的方式决定的。当你自己实现过分词、引号匹配、重定向解析你就知道 Shell 对于空格和引号有一套非常死板的处理逻辑。你写的每个脚本最终都要被一个和你写的相似的解释器逐字符拆开解读。这就是为什么自己动手写解释器是理解 Shell 脚本的最佳捷径。4.4 推荐开启的功能扩展清单如果你做完第一版觉得还不过瘾这几个扩展方向按难度递增支持历史记录用readline库半页代码搞定上下键历史支持通配符展开*.c需要自己遍历目录获取匹配文件列表支持环境变量展开$HOME的替换本质是在分词阶段加一个状态分支支持作业控制后台运行、jobs、fg、bg。这是最接近“工业级”的扩展也是难度跳跃最大的一项支持双管道让cat file | grep foo | wc -l能工作。需要设计一个递归解析结构每个管道段都当成“左侧”的子命令其中第五个扩展我强烈建议尝试。它要求你把解析器从“左右两段”改成“多段”需要维护一个命令链。很多 shell 脚本里复杂的管道组合都是这条链路撑起来的。5. 最终效果与个人体会最后我的这个“自制 Shell”支持了这些功能执行外部命令支持 PATH 搜索内建命令cd、exit、pwd、export输入重定向、输出重定向和追加重定向单管道连接两条命令参数里的引号解析简单情况环境变量赋值如FOObar ./cmd退出状态码返回$?的基本概念最直观的测试方式是写一个递归的测试脚本比如让 shell 自己执行输入重定向里的命令验证它跟 bash 行为是否一致。我当时的验收标准是同一个命令字符串在 bash 里运行和在我的 mysh 里运行结果应该逐字相同。这个标准定的很朴素但也最扎实。做这个项目期间我最深的体会是Shell 不像一个“应用程序”更像是一个“胶水层”它把 Unix 的几个核心抽象——进程、文件描述符、环境变量、信号——干净利落地组合在一起。你只有亲手实现一遍才真正感知到那些天天都在用的概念到底是怎么工作的。最后再分享一个小技巧调试这个项目最有效的工具不是 GDB当然 GDB 也重要而是在每个关键节点fork 前后、dup2 前后打印一条日志。因为 fork 之后子进程的执行路径和父进程完全不同GDB 跟子进程天然有点别扭直接打日志看“谁 fork、谁 exec、谁退出”反而一目了然。动手吧中途卡住是正常的做完这个项目再看 shell 脚本的世界完全是另一番景象。

相关新闻

SpringBoot+Vue船运物流管理系统:从源码到答辩全流程实战解析

SpringBoot+Vue船运物流管理系统:从源码到答辩全流程实战解析

手里攒了一套SpringBootVue的船运物流管理系统平台完整源码,附带SQL脚本和接口文档,适合做Java Web毕设或者练手二次开发。这几年找我咨询这类项目的同学不少,大家最关心的问题高度一致:源码拿到了,怎么快速跑起来&…

2026/10/11 0:16:38 阅读更多 →
HarmonyOS跨端数字画板实战:协同创作与云同步架构解析

HarmonyOS跨端数字画板实战:协同创作与云同步架构解析

让我们把目标聚焦在一点上:在 HarmonyOS 6.0 生态下,做一个真正能用的跨端数字画板,并且把多设备协同创作和云端同步这两块硬骨头啃下来。这个标题看起来像是需求文档里的宏大愿景,实际开发时会发现,画板本身不难&…

2026/10/11 0:16:38 阅读更多 →
VideoJAM:用运动感知联合表示攻克视频生成运动一致性难题

VideoJAM:用运动感知联合表示攻克视频生成运动一致性难题

简介:这是一套面向中级 JavaScript 与 Electron 开发者的 VideoJAM 项目起步代码,基于官方快速启动模板改造,适合需要快速搭建视频处理桌面应用、或想了解 FFmpeg 与打包工具集成方式的读者。项目同时覆盖主进程与渲染进程逻辑、前端组件、页…

2026/10/11 0:16:38 阅读更多 →

最新新闻

WPF贝塞尔曲线绘制平滑折线图实战指南

WPF贝塞尔曲线绘制平滑折线图实战指南

简介:本资源是一个基于WPF与C#实现的贝塞尔曲线动态折线图可视化项目,面向.NET桌面开发初学者及图形学实践者,解决传统折线图缺乏平滑过渡与动态量程适配的问题。项目完整封装为RAR压缩包(65KB),共36个文件…

2026/10/11 1:51:41 阅读更多 →
视黄酸、FIV与脑膜屏障——猫原代脑膜细胞如何解码神经发育与神经免疫的交叉调控密码

视黄酸、FIV与脑膜屏障——猫原代脑膜细胞如何解码神经发育与神经免疫的交叉调控密码

在神经科学研究领域,脑膜长期以来被视为静态包裹大脑的“惰性保护层”。然而,近十年的研究正在从根本上改写这一认知——脑膜不仅是中枢神经系统的物理屏障,更是一个高度分区化、功能特化的动态微环境调控系统。脑膜成纤维细胞主动表达与血脑…

2026/10/11 1:51:41 阅读更多 →
功能红利退潮之后:C端产品设计差异化的4条走心路径

功能红利退潮之后:C端产品设计差异化的4条走心路径

【摘要】功能差异的保质期已缩短至6个月,参数升级的用户感知趋近于零,C端差异化竞争正从功能层转向情感层。文章以波特竞争战略、KANO模型、峰终定律为锚点,用走心力4因子公式拆解4个落地切口,配套6步执行流程、3类典型误区与3条量…

2026/10/11 1:51:41 阅读更多 →
免疫组库基础分析14:基于NAIR包的TCR/BCR免疫组库公共簇分析

免疫组库基础分析14:基于NAIR包的TCR/BCR免疫组库公共簇分析

摘要 适应性免疫受体库测序(AIRR-Seq)是解析免疫应答、疾病标志物筛选的核心技术,TCR/BCR簇的跨样本共享性与表型关联性是关键研究切入点。NAIR(Network Analysis of Immune Repertoire)是基于R语言的免疫组库网络分析…

2026/10/11 1:51:41 阅读更多 →
Node-Exporter 详解:服务器监控神器,从零部署实战教程

Node-Exporter 详解:服务器监控神器,从零部署实战教程

文章目录Node-Exporter 详解:服务器监控神器,从零部署实战教程一、什么是 Node-Exporter?核心监控范围二、为什么要用 Node-Exporter?三、Node-Exporter 部署实战(两种方式)方式一:二进制部署&a…

2026/10/11 1:51:41 阅读更多 →
主动悬架真正难的并不是算法

主动悬架真正难的并不是算法

前言 做主动悬架时间久了,有一个很深的感受: 主动悬架真正难的,往往不是算法。 刚开始接触这个领域时,很容易把注意力集中在控制算法上。Skyhook、LQR、H∞、MPC,甚至更复杂的预测控制和整车协同控制,看起来…

2026/10/11 1:50:41 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →