全部收到。现在开始输出。
1. 为什么我会动手写一个Markdown批量处理小工具
先交代一下背景。我平时维护一个个人知识库,里面的笔记、技术文档、周报草稿全部都是用Markdown写的。写了一两年之后,文件数量到了上千的量级,问题就跟着来了——你光靠手动去改这些文件,根本改不过来。比如某天我想把所有文档里的旧域名批量替换成新域名,或者在每一篇笔记顶部统一加上一个标签块,再或者把某几个文件夹下的.md文件全部转成更规范的格式。这些需求听起来都挺简单,但放在几百上千个文件里,就不是人肉能干的活了。
其实市面上的现成工具不是没有。拿批量替换来说,VS Code里带全局搜索替换,很多编辑器也支持多文件正则替换。但实际用下来你会发现几条痛点:
第一,编辑器里的替换往往是一次性的,操作步骤不可复用。今天替换一个域名,明天追加一行tags,后天又要统一一下标题层级,每次都得重新配一遍规则,很烦。
第二,光替换文本是不够的。很多时候你想做的是结构化操作,比如读取每个md文件的标题,再把标题转成文件名、给特定段落加锚点、检查有没有重复的标题等等。这种"既要读内容又要改内容"的需求,纯靠编辑器的查找替换根本做不了。
第三,我需要它能自动化跑起来。比如每个月初自动给上个月的笔记归档,给所有没有tags的文件补上默认标签,这种操作最好一条命令搞定,而不是每次打开编辑器手动来。
所以我就打算自己写一个命令行小工具,专门做Markdown文件的批处理。说实话,市面上确实也有类似功能的开源库,比如python-markdown里带的部分工具链,或者一些专门的md lint工具。但大部分要么功能偏检检查不偏修改,要么配置复杂得跟写程序一样。我想要的是一个自己完全能掌控、能按我的习惯不断加功能的工具,所以从头写反而更符合我的场景。
这篇文章就打算把我这个工具的完整想法、具体实现和踩过的坑都写清楚。如果你也有一堆Markdown文件需要批量整理,或者你想对文件批处理这件事有个更系统化的思路,这篇应该能给你一些参考。哪怕你不太会写代码,照着思路去找AI辅助或者让懂行的朋友帮你实现,也完全可以。
2. 我最终敲定的技术方案和设计思路
2.1 为什么选Node.js而不是Python或Shell
最开始我其实想的很简单,用Shell脚本加一堆sed、awk命令就能把批量替换搞定。试了一下很快就放弃了,因为涉及到多行匹配、中文编码、大小写敏感替换这类问题的时候,sed写起来实在痛苦,规则稍微复杂一点就成了一坨没人看得懂的"咒语"。后面我又考虑用Python,Python做文本处理当然很强,但在"把命令行工具做成一个长期维护、不断增加功能的小项目"这个场景下,Node.js对我反而更顺手。
原因有三:一是JavaScript对JSON格式天然友好,我的配置文件直接写成JSON或者CJS模块就行,Python还得捣鼓yaml或者json库;二是Node.js的生态里,修改文件、遍历目录、处理命令行参数都有非常成熟的库,npm install一下就能用;三是我的知识库工具链本来就是基于Node.js的,比如说我用eleventy或者vitepress这类静态站工具建站,那用同构的思路去写周边工具,维护起来成本最低。
如果你完全不懂JavaScript,拿Python写也完全没问题,核心思路是一样的:先确定你的需求是"规则驱动的批处理",然后选一个能方便操作文件系统、能处理正则表达式的语言。
2.2 整体架构:规则驱动,而不是功能驱动
我之前见过很多小工具最后变得很难维护,根本原因是把功能写死了。比如今天加一个"替换域名"的功能,代码里就出现一个replaceDomain()方法;明天加一个"添加tags"的功能,代码里又出现一个addTags()方法。功能越来越多,代码越来越乱,最后变成一坨谁也看不懂的逻辑。
所以这次我从一开始就坚持一个原则:把整个系统拆成"扫描器"和"转换规则"两部分,扫描器固定不变,转换规则全部通过配置来定义。
具体来说,工具的骨架是这样的:
一条命令执行时,会先读取配置文件,配置文件里声明了一组规则。每条规则包含三个部分:一是匹配哪些文件,二是对文件内容做什么操作,三是操作完成后要不要再做一些额外动作。
// 配置示例,这个文件就是整条流水线的"图纸" module.exports = [ { name: "replace-old-domain", fileMatch: ["docs/**/*.md", "notes/**/*.md"], type: "regexReplace", options: { pattern: "old-domain\\.com", replacement: "new-domain\\.com", flags: "gi" } }, { name: "inject-default-tags", fileMatch: ["notes/**/*.md"], type: "prependIfMissing", options: { pattern: "^tags:", inject: "tags: [未分类]\n" } } ];这种设计最大的好处是:以后我想加新的批处理能力,大多数时候只需要增加一个新的规则类型,或者写一个新的处理函数,然后在配置里声明怎么用就行。工具本身不会越来越臃肿,它始终是一个"跑规则"的引擎。
2.3 文件遍历:哪些坑要提前绕开
文件遍历是整个工具的地基。地基如果歪了,后面全部白搭。我先说一下我踩过的几个具体的坑:
第一个坑是依赖文件在遍历后再被修改。比如我设置了一条规则把所有md文件里的小写标题改成大写标题,遍历完之后,文件名本身可能没变,但文件的内容已经变了。如果第二条规则还要依赖"文件内容里的目录结构"去做操作,那必须重新读取文件,不能直接用内存里缓存的旧数据。
第二个坑是遍历时的文件排序不稳定。JavaScript的readdir方法在不同操作系统上返回的顺序并不固定。如果两条规则之间有先后依赖关系,比如第一条规则统一在文件头部插入一个frontmatter块,第二条规则解析这个frontmatter块,那么文件处理的先后顺序一旦乱了,结果就可能不对。所以我在实现的时候把扫描结果做了一次显式排序,让所有文件的处理顺序始终稳定。
第三个坑是符号链接和隐藏文件的处理。如果你的笔记目录里有软链接指向其他目录,一个不小心,工具就会跑到目录外面去处理不该动的文件。我在遍历时默认跳过dot开头文件和符号链接,同时把真实路径显示在dry-run日志里,方便用户一眼看到工具到底会对哪些文件动手。
这里放一个我最终使用的遍历逻辑简化版:
const fs = require("fs"); const path = require("path"); function walk(dir, exts = [".md"]) { const results = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); entries.sort((a, b) => a.name.localeCompare(b.name)); for (const entry of entries) { if (entry.name.startsWith(".")) continue; if (entry.isSymbolicLink()) continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...walk(fullPath, exts)); } else if (entry.isFile() && exts.includes(path.extname(entry.name))) { results.push(fullPath); } } return results; }这个函数看起来简单,但注意里面有一个关键设计:先排序后递归,且对目录和文件都做了过滤。这样工具跑起来之后行为是可预测的,不会出现"这次处理了那个文件,下次没处理"的随机情况。
3. 核心模块的实现细节,从读取到改写
3.1 三态流程:dry-run、diff、write
做文件批处理工具,最忌讳的就是一上来就真改文件,把一堆文件改坏。我的做法是把一次执行分成三个明确的状态。
第一个状态是dry-run,只扫描、只计算,但完全不写任何文件。工具把所有计划执行的操作列出来,比如"将把10个文件中的old-domain.com替换为new-domain.com",然后输出一个列表,列清楚哪个文件会被改动、改动几处。
第二个状态是diff模式,这就比dry-run更进一步了:工具会把每个文件修改前和修改后的内容差异打出来,像git diff那样显示每条具体改动。这个模式对"规则编写是否正确"的验证价值很大。说实话我一个周末的下午就靠diff模式抓出过好几条自己写错的正则。
第三个状态才是真正执行。默认情况下,工具要求你加一个--apply或者-y参数才会真正写入文件。写入之前还会再把文件整体备份到.backup目录,防止出意外。这个备份机制不是强迫你永远保留这些备份文件,而是给你一个"后悔药",确认没问题之后手动删掉就行。
用命令行参数来控制这三个状态,我最终的接口设计长这样:
node tool.js --config my-rules.js --dry-run # 只看统计 node tool.js --config my-rules.js --diff # 看具体每条diff node tool.js --config my-rules.js --apply # 真正写入3.2 规则引擎的核心:如何设计一个可扩展的规则类型
我上面提到,转换规则是整个工具的灵魂。所以规则类型的设计必须认真想清楚。我目前实现了五类基本规则,覆盖了我自己的绝大多数需求:
第一类是文本替换规则,本质是正则替换。这里有个重要的点:我不建议只提供"全局替换字符串"这种过于简单的能力。实际场景里你往往想要条件替换,比如只在标题行里替换、只在链接文本里替换,这就需要允许在规则里配置"行匹配模式"和"行内替换模式"两层逻辑。
第二类是行级过滤规则,用来删除包含特定内容的行,或保留符合特定条件的行。例如把文档里所有包含"TODO: 删除"的行筛掉。
第三类是前置/后置插入规则,用来在文件头部或尾部注入内容。典型的场景就是给文件补frontmatter,或者在每篇文档末尾追加一行"最后更新时间"。
第四类是结构转换规则,专门针对Markdown的标题、列表、代码块做操作。比如把所有二级标题统一改成三级标题,把无序列表前导符号从-统一成*。
第五类是文件名操作规则,可以根据文件内容里的某个字段重命名文件。
其实现在这个列表已经覆盖了90%的日常批处理需求,但我仍然保持了扩展接口。新规则类型不需要改主引擎,只需要实现一个类似transform(content, context)的函数,然后在规则列表里注册即可。这又回到我前面说的架构原则:引擎做调度,规则做操作,二者解耦。
3.3 多个规则叠加时,怎么保证结果可预期
有了多个规则之后,执行顺序就成了一个必须处理的问题。我这里说的不只是"先执行规则A再执行规则B",而是更细一层:规则执行顺序改变会不会影响最终结果?
举例来说,你有一个文件里写着"欢迎访问http://old-domain.com",你想同时做两件事:把old-domain.com换成new-domain.com,以及给所有http链接加上https。如果你先替换域名再做协议替换,结果可能是"https://new-domain.com"。但如果你先做协议替换,原来的http还没被识别成链接,链接格式化规则可能就漏掉了这一条,最后变成"http://new-domain.com"加一个本来就有的http,效果就不对。
所以我在工具里明确支持规则的"阶段"概念。每条规则可以声明自己的阶段,比如第一阶段做规范化、第二阶段做替换、第三阶段做注入。执行时先按阶段分组,再组内按顺序执行。这样虽然还是线性执行的,但至少能表达"这一类操作必须等另一类操作做完之后再做"的意图。
如果你不去处理这个问题,短期可能看不出毛病,因为你自己知道哪些规则不能同时开。但随着规则数量增加,你迟早会忘,然后就会在某次批量操作里产生一堆"半转换"的文件。这个我在测试阶段就深有体会,所以宁可把顺序机制做复杂一点,也不愿意留这种隐患。
4. 实测中的意外情况和排查链路
4.1 意外之一:中文文件名和编码问题
第一版工具跑起来之后,我最先遇到的就是中文相关的问题。我知识库里的文件名不少是中文的,比如"前端性能优化笔记.md"、"临时想法2023-备份.md"。文件遍历本身没问题,但是当我尝试在终端日志里打印文件路径的时候,Windows终端和Linux终端显示出来的格式不一样,有的地方还会输出成乱码。这不是文件内容出问题,纯粹是终端的字符编码与工具输出之间的编码冲突。
排查链路是这样的:我一开始以为是遍历代码出错了,后来单独写了一个测试脚本,先读取文件名打印长度,发现长度正确,但显示出来是乱码。接着我在脚本顶部强制声明UTF-8编码,并通过Buffer把路径转成十六进制打印,确认底层字节是正确的。最后定位到是终端代码页的问题,Windows里chcp 65001切到UTF-8即可解决,而Linux服务器上则是locale没配对,导致终端显示用了其他编码。
处理方案有两个层面。第一个层面是工具本身:我把所有读取写入都强制指定UTF-8编码,不让系统默认编码参与进来,fs.readFileSync(file, "utf8")和fs.writeFileSync(file, content, "utf8"),这点必须做的。第二个层面是运行环境:在工具文档里注明Windows用户建议在PowerShell里先执行chcp 65001,Linux用户确保LANG=en_US.UTF-8或zh_CN.UTF-8。
第二个坑是文件内部的\r\n换行符。我在Windows上编辑过一部分文件,所以这些文件的换行符和Unix文件不同。当我做正则替换时,如果正则是$匹配行尾,在\r\n环境下匹配行为就会和预期不一致。后来我的做法比较简单粗暴:读取文件时统一把\r\n转成\n,处理完写入时再根据配置决定输出\n还是\r\n。这样规则表达式不用考虑换行差异,逻辑干净很多。
4.2 意外之二:一个正则表达式差点毁掉所有note文件
这个必须要单独说,因为这是我在这个项目里最惊险的一次事故。
我在给一个规则配置正则时,本来想匹配的是"所有以#开头的一级标题",然后统一改成"一级标题后面加一个分隔线"。正则我写成了^#{1}\s,这个本身没有问题。但是我没有加m多行标志,所以正则默认只在字符串开头匹配。而在JavaScript里,没有m标志时,^只匹配整个字符串的开头。结果就是只有第一个文件的第一行被处理了,其他文件全都原封不动。这个虽然不对,但好在影响范围小,不算灾难。
但让我后怕的是另一次:我把规则写成了^#.*,本想匹配标题行,但这个正则加上g标志之后,会把文件中所有以#开头的行全部匹配到,包括Markdown里用于注释或代码块中的#内容。如果当时加了--apply直接跑,一批笔记里的代码示例和注释就会被错误地插入分隔线。我全靠diff模式及时发现了这个问题,才避免了一大批文件内容被污染。
整理成教训就是三条:一是只用diff模式观察足够多样本之后再去apply;二是所有正则都要考虑多行标志、转义字符和贪婪匹配的影响;三是对正则拿不准时,用一个独立测试脚本单独跑一遍,不要直接丢进批量工具里试。这个排查链路我完整走下来花了两个多小时,最后发现根因就是一行正则,真的是典型的"越简单的错误越迷惑人"。
4.3 意外之三:文件被外部程序占用导致写入失败
还有一个很实际的问题:我在Windows上开着Typora或VS Code编辑某些md文件时,如果这时候运行批量工具去写入这些文件,偶尔会碰到EBUSY错误或者EPERM错误。具体表现是工具跑了半天,突然卡在一个文件上报错,然后中断。如果前面已经写入了上百个文件,这就会造成"改了一半"的不一致状态。
我的解决思路分两道:第一道是执行前检查,遍历所有目标文件,如果发现文件被占用就跳过并报告,而不是硬写;第二道是写入失败时的整体回滚机制,虽然实现起来不复杂,但效果很好——准备写入前先把目标文件内容存到内存,如果后续文件写入失败了,就把之前已经写过的文件挨个恢复。这个机制我觉得是批处理工具必备的,任何生产级的批量修改工具都应该有,不然你永远不敢放心地去跑大范围操作。
5. 实用功能扩展:从单纯替换到更强的批量管理
5.1 统计与报表:知道自己整理了多少文件
工具初期只能做替换和注入,但用着用着我发现,还有一个更高频的需求是"统计信息"。比如我想知道笔记库里一共有多少篇带tags的,多少篇没带;或者是这个月新增了多少篇;又或者哪些文件里包含外部链接。这些信息可以帮助我决定还要不要做批量操作。
所以我给工具加了一个--report模式,它不会改写任何文件,只输出一个汇总报告。报告内容可以配置,比如输出文件总数、按年份分组的数量、标签统计Top10、无标签文件清单等。这个功能看似简单,但实际使用率极高,因为它把"管理一个巨大知识库"这件事从"打开文件一个个看"变成了"一条命令出一份体检报告"。
5.2 文件操作安全机制:备份、回滚、黑名单
文件批处理其实是一个"高风险低频率"的操作。你说一年到头能跑几次?可能就几次,但每次跑错了都是大事故。所以我在这部分花了不少功夫。
除了上面说的占用检查和自动回滚,我还加了三个安全性设计:
第一个是黑名单机制。某些文件无论如何都不允许被批量修改,比如README.md、索引文件、包含"archive"字样的目录。黑名单可以直接硬编码在配置文件里,任何规则都不能越过它。
第二个是自动备份目录。每次执行apply之前,工具会把所有即将被修改的文件复制到一个时间戳备份目录里。备份不是覆盖式的,是完整保留修改前的内容,这样就算事后后悔了,也能一条命令恢复所有文件。
第三个是执行日志。每次运行结束,工具都会生成一份执行日志,包含处理了哪些文件、每条规则命中多少次、耗时多少这类信息。第一次看我可能觉得用处不大,但你真的碰到"上周跑过一次批量替换,这周又跑了一次,结果文件乱了不知道是哪一次跑坏的"这种问题的时候,日志就是救命稻草。
5.3 为已有知识库定制规则:三个真实案例
光说设计理念太虚了,我拿三个真实场景给你展示一下规则到底怎么配。
第一个场景:给所有旧笔记补frontmatter。我早期写的笔记根本没有frontmatter,但现在静态站构建工具需要每篇笔记都有tags和date。我写了一条前置插入规则,先按文件名中的日期正则尝试提取date,如果没有提取到,就默认用文件的创建时间,然后在该文件顶部补上date: xxxx-xx-xx,再补一个默认tags。这一步做完,几千篇笔记全部规范化了。
第二个场景:把旧的站内链接格式迁移到新格式。我很久以前用的站内链接写法是[[文章标题]],后来新系统要求的是[文章标题](/path/to/file)。这个转换本质上需要"读取标题->查文件名映射表->替换链接"三步。我在规则引擎里加了一个linkMigration类型,它接受一个映射表JSON,然后逐行扫描,把所有匹配到旧格式的链接替换成新格式,并输出一个无法自动映射的链接列表供人检查。
第三个场景:删除所有"待办"标记的整段内容。我的笔记里以前习惯用"todo"代码块来记录待办事项,时间长了积累了太多已经没有用的内容。我写了一条行级过滤规则,从遇到"todo"开始到""结束,这一整段全部删除。这条规则配合dry-run模式让我直观看到了将要被删的内容,最终确认无误后一次性清理干净。
这三个场景让我比较确定的体会是,批处理工具的价值其实不在于"一次替换所有文件"这个动作本身,而在于它把"整理知识库"的整个工作流,从手动、零散、不可恢复,变成了自动、可配置、可回滚。这才是它真正帮我省时间的地方。
6. 性能表现和优化方向
6.1 一次性处理上万文件的耗时实测
很多人会担心,JS处理成千上万个文件,性能会不会不行?我拿自己的知识库实际测过。我的库里有大约12000个md文件,平均每个5KB左右,跑一次全量文本替换(不含备份、纯替换写入),全程耗时大约是4秒左右。如果开启备份,耗时大概翻一到两倍,因为多了大量文件复制IO。
这个结果对我来说完全够用。像"批量给所有文件添加tags"这种操作,从跑到验证完,整个流程不会超过半分钟,比手动打开编辑器一个个改不知道快到哪里去了。
当然如果你有数十万甚至百万级文件,性能瓶颈会出现在两个地方:一是遍历目录时逐步递归的速度,二是写入时单文件同步IO的延迟。百万级场景我会建议改成使用异步读取批量并发,或者直接用worker_threads把文件拆分到多个线程去处理。但这些对于个人知识库场景基本用不上,我也不建议在早期优化这些。
6.2 局部增量处理:怎么让工具跑得更聪明
目前的工具每次都是全量扫描。但对于一个知识库来说,大部分文件其实是不需要经常处理的。所以我正在做的一个优化方向是:基于文件的最后修改时间做增量处理。思路很简单——遍历时记录文件mtime,如果在规则配置里加了"只看最近修改的文件"这个条件,就只处理mtime晚于指定日期的文件。这样每次跑批处理就只处理最近的变更文件,整个流程能在2秒内完成。
另外一个方向是文件级缓存。如果某个文件在最近一次规则运行之后没有被修改过,且规则本身也没变,那就可以直接跳过处理。这个用文件哈希做最简单,但对我来说用处不大,因为我的规则数量不多,全量扫描也就几秒钟,不需要过度设计。
7. 版本管理和配置模板:让工具可持续演进
7.1 规则文件用什么格式最方便
我建议规则文件不要硬编码在工具代码里,而是独立成单独的配置模块。这样你换了一个知识库目录,直接换一套规则就行,工具本体不用动。目前我用的是CommonJS模块导出方式:
module.exports = { scanDir: "./docs", extensions: [".md"], backupDir: "./.backup", rules: [ // 规则列表 ] };之所以选择CommonJS而不是JSON,是因为规则里经常要写正则表达式。JSON里存正则很不方便,要么转义要么用字符串再编译,麻烦且容易出错。CJS模块允许我的配置里直接写/pattern/flags这种原生正则,阅读起来和调试起来都直观得多。如果你以后想把这个工具分享给别人用,觉得CJS有门槛,也可以再提供一层JSON配置并用函数做一层转换,这是一个取舍问题,没有绝对的对错。
7.2 给工具做自动化测试
有人可能会觉得,写一个小工具还做什么测试,直接跑不就行了。但对文件操作工具来说,自动化测试反而是最值得做的那部分。因为我发现,文件处理的bug往往不是逻辑复杂,而是边界情况太多。空文件、只有BOM的文件、全都是空行的文件、没有结尾换行的文件、超大单行文件,任何一项都可能让规则引擎挂掉或产生奇怪结果。
我的做法是建了一个test目录,放了几十个样本md文件,覆盖各种边界。然后写了简单的测试脚本,每条规则跑一遍样本文件,校验输出是否符合预期。一旦某个规则改动过,就立刻跑一轮测试,确保没有破坏其他规则。这个看起来麻烦,但能帮你在大批量跑之前拦截绝大多数低级错误。我强烈建议任何打算做类似工具的人都把这个步骤加上,哪怕测试写得很粗糙,也比完全没有强。
7.3 工具的演进思路:从一次性脚本到可复用流水线
这个工具的下一阶段,我想把它往"流水线"方向再推进一步。目前的版本是一条命令执行一组规则,已经很好用了。但我设想的进阶版本是:把规则按阶段组织成流水线,比如"清理阶段"先做格式规范化、再做内容替换,最后进入"注入阶段"统一补元数据。每个阶段可以单独运行、单独查看报告,这样对更大规模知识库的管理会更有掌控力。
另一个想做的扩展是插件化。让其他用户能基于公开接口自己写规则类型,而不需要改我的主框架代码。目前这个版本已经做了一部分基础工作,规则类型是通过接口注册的,所以后续扩展会比较自然。不过这个优先级不高,个人工具够用最重要,不能为了做架构而做架构,复杂度要在真正需要的时候再加。
8. 安全边界和批量操作时的纪律
8.1 写给自己的操作纪律
用这种工具跑批处理,本质上是用自动化去替代人肉操作。自动化带来效率的同时,也带来了"人肉操作时本来会有的谨慎"。手动编辑时,你每改一个文件眼睛都会过一遍,但批量工具可能几秒钟改了几千个文件,你根本没机会看细节。所以纪律非常重要。
我现在给自己定的规则很简单,也不复杂,甚至有点保守:任何批量操作前,必须依次执行dry-run、diff、抽查三个步骤。dry-run看统计数量,diff看详细差异,抽查则随便打开三五个文件看看实际内容是否合理。三步全部通过之后才会真正运行apply。如果涉及删除内容或文件重命名,我还会单独再检查一遍黑名单配置。
这个纪律可能听起来繁琐,但真的一次事故就能让你追悔莫及。像我之前那次正则写错差点毁掉所有笔记,要不是先跑了diff再抽查,大批文件就会变成不可恢复的状态。文件操作工具做得再多安全机制,也不如操作者自己养成良好习惯来得可靠。
8.2 哪些情况不适合用工具处理
我也要泼一点冷水。不是所有文件操作都适合做成批量规则。这个边界我越来越清楚——凡是"判断标准模糊、需要人工理解语义"的操作,都不适合批处理。比如说你想把笔记里所有过时信息整理掉。什么叫过时?这个判断可能涉及上下文理解、涉及你对当前工作目标的主观判断。这种活强行做成规则也会错漏百出,不如老老实实人工过。
反过来,凡是"格式明确、规则可描述、差异可审计"的操作,比如统一标题层级、替换固定字符串、补全元数据,这类就非常适合批处理。工具的价值应该集中在这些确定性操作上,而不是试图替代人的理解和判断。
8.3 出错后的恢复策略
最后说说出现事故后的处理。我设计工具的时候按顺序做了三级恢复策略,优先级从高到低。
最高优先级是通过备份目录恢复。只要执行过apply,备份目录里就有修改前的文件。恢复方式很简单,把备份目录的文件复制回原目录即可。这也是我优先推荐使用备份目录的原因。
如果备份目录也出了问题,那么第二步是用git。如果你的知识库本身被git管理,那git checkout就是最好的恢复方式。所以我前面建议,即使知识库不需要团队协作,也建议初始化git,它不仅是版本管理工具,也是文件批量操作的最后一道安全网。
如果连git也没有,第三个办法就只能靠编辑器或系统的回收站碰运气了。但这种方法基本不可靠,所以真正靠谱的结论是:**在使用批处理工具之前,先确认你的数据有一个可以恢复的渠道。**没有这个前提,任何批量操作都是在冒不必要的风险。
我自己在实际使用中最大的体会就是:这个工具最核心的价值,不在于把"改文件"本身做得多快,而在于把"修改前的验证"和"修改后的恢复"这两件事做得足够扎实。工具跑得再快,一次误操作都可能让你一整年的笔记积累遭殃。反过来,只要有diff和备份这两道护城河,哪怕中间真的出了错,也能在几分钟内恢复原状,那你就敢更大胆地去用自动化解放自己的手脚了。最后再分享一个小建议:如果你也要做类似的工具,第一版不要急着加功能,先花时间把dry-run、diff、备份、回滚这四个安全机制做扎实,这绝对是整个项目里最值回票价的部分。