那天晚上我盯着屏幕,看着一整屏红色的报错,有点哭笑不得。
报错是这样的,一个接一个:
command not found: aside
no such file or directory: /aside
command not found: auto-update
command not found: checkpoint
command not found: code-review
……密密麻麻,几乎停不下来。
奇怪的是,我根本没有去运行这些命令。 我只是想写个脚本,把我平时用的 75 个 AI 命令,批量生成一组简短的"快捷别名",好让我少敲几个字。结果脚本一跑,这 75 个命令名,全部被系统当成了真的命令,挨个执行了一遍,挨个报错。
一个本该帮我省事的脚本,差点把我用了很久的配置搞崩。
事情的起因
我平时用 AI 干活,会调用很多自定义的技能,每个都有名字。问题是这些名字有点啰嗦,比如某个技能实际叫 source-command-pr,可我想敲的是简短的 pr。
一个两个好办,手动建就行。可我手里有 75 个这样的命令。手动建 75 个别名,太蠢了,于是我写了个脚本,让它自动遍历这些技能,给每个技能生成一个简短名字的副本。
思路完全正确。错的是脚本里一个不起眼的符号。
元凶:反引号
我在脚本里,用了反引号(就是键盘左上角、数字 1 旁边那个 `)。
在我看来,反引号只是个普通字符,我想把它写进生成的文件里。但在 shell(命令行解释器)的眼里,反引号有个特殊身份:它表示"把里面包着的内容,当成一条命令先执行,再把结果填回来"。
这就是著名的"命令替换"。
于是悲剧发生了。脚本里那些本该被当成纯文本的命令名,因为被反引号包着,shell 一看,以为我要运行它们,就真的去执行了。aside 被当成命令运行、auto-update 被当成命令运行、checkpoint、code-review…… 75 个名字,一个不落。
它们当然不存在,于是屏幕上刷出一长串 command not found。
这是最容易被忽略的那类错误:不是逻辑错了,而是你以为无害的一个字符,在工具眼里是另一个意思。 就像你在某些场合说一句普通的话,可对方的语境里,那句话是个指令。脚本世界里,这种"一词多义"的符号特别多。
麻烦不止报错
如果只是刷一屏报错,我还能笑一笑就过去了。但脚本是在我的核心技能目录上跑的,问题就没那么简单。
第一个麻烦,是部分文件没改干净。脚本本意是为每个副本生成正确的头部信息(行话叫 frontmatter),结果中途因为反引号翻车,有些文件生成了一半,头部信息残缺不全。
第二个麻烦,是早就存在的一些坏链接,被一并翻了出来。 我那个目录里,有十几个指向某个外置卷的符号链接,而那个卷当时没连上,链接是断的。平时它们静静躺着,可脚本一扫描、一处理,扫描器就容易卡在这些断链上,甚至误判。
第三个麻烦,是最要命的:这种自动化脚本一旦把核心目录污染了,你很难一眼看出到底哪些文件被改坏了。 75 个文件批量生成,有的对、有的错,混在一起,排查成本远高于手动建。
万幸的是,它动的是"副本"
等我冷静下来,我意识到这次能补救,有个关键原因:我那个脚本是在"生成新的副本",而不是"修改或删除原始文件"。
我那些真正的技能本体,名字是完整的、内容是好的,脚本只是在它们旁边造了一批简短名字的副本。副本造坏了,本体还在,大不了把副本推倒重来,伤不到根本。
这个区别,值得每个写批量脚本的人想清楚:一个脚本的危险程度,往往不取决于它出不出错,而取决于它出错时,动的是"原件"还是"复印件"。
如果我当时图省事,让脚本直接对原始文件做批量替换、批量改名,那后果就完全不同了——反引号的问题一旦触发,被搞坏的就是 75 个命令的本体,而且可能在我反应过来之前就改完了,连个对照的正常版本都没有。
所以我后来给自己定了个规矩:能让脚本在副本上操作的,就别让它直接动原件;非动原件不可的,先完整备份、先小范围试。 给操作留一个"原件还在"的安全垫,翻车时才不至于满盘皆输。
我没有直接删掉重来
这时候有个很冲动的做法:把刚生成的东西全部删掉,重新手动来。
我没这么干。因为我不确定脚本到底碰了哪些文件,贸然全删,可能把原本正常的东西也误删了。
我做的第一件事,是让脚本停下来,然后弄清楚它到底干了什么。 我先把那批指向失效卷的断链,移动到一个备份目录,让它们不再干扰扫描;接着,我逐个核对脚本生成的文件,把头部信息残缺的挑出来。
第二件事,是重新写生成逻辑,这次用"安全引用"。 这是关键。所谓安全引用,就是把所有可能被 shell 特殊解读的内容(反引号、美元符号、各种括号)都正确地包裹起来,让它们老老实实当文本,绝不被当成命令执行。
同一个目的,第一次写的脚本把特殊字符当指令,第二次写的脚本让它们当文本,差别就在这层引用上。
第三件事,是改完之后,先小范围试一两个,确认生成的文件完全正确,再批量处理剩下的。 我还跑了一遍验证,确认所有副本的头部信息干净、名字对应、没有断链干扰,这才算真正修好。
修完之后,我还做了一个小动作:把这次踩的坑——"反引号会被当命令执行、脚本要先小范围试"——记进了我的迁移经验里。我不想以后再遇到同样的问题,还得靠那一屏报错来提醒自己。 一次翻车,最好能换来一条以后自动生效的规则,否则这跟头就白栽了。
这种坑,根子上是"太信任自动化"
复盘整件事,我发现真正的问题,不是我不会写脚本,而是我默认了"批量自动化"就等于"安全高效",跳过了本该有的小动作。
我跳过了什么?
一是没有先小范围验证。 我让脚本直接对 75 个文件、对核心目录全量执行。如果我先拿一两个命令试跑,反引号的问题立刻就会暴露,损失也就一两个文件。
二是没有给操作留退路。 批量改之前,我没有先备份、没有确认这些改动能不能轻松回滚。结果一旦改坏,只能手忙脚乱地排查。
三是没有提防脚本里的特殊字符。 反引号、美元符号、星号这些,在 shell 里都有特殊含义。让 AI 帮你写脚本很方便,但越是一句话生成的批量脚本,越要留意它有没有正确处理这些字符。
下次我会怎么做
下次再写批量处理脚本,我会守住这几条:
第一,先 dry-run(空跑)。 让脚本先告诉我"我打算改哪些文件",而不是上来就动手。
第二,先小范围试跑一两个,确认结果正确,再放开批量。核心目录绝不当第一个试验场。
第三,操作前先备份,确保能回滚。 改坏了能一键退回,心里才有底。
第四,对特殊字符保持警惕,所有可能被 shell 解读的内容都用安全引用;脚本里要生成什么文本,就确保它只被当文本。
第五,顺手清理环境里的断链和坏链接,别让脚本在这些陈年隐患上翻车。
说到底,自动化是个好东西,但它放大的不只是效率,还有你每一个疏忽——一个没写对的符号,在手动操作时顶多错一次,放进批量脚本里,它能错 75 次。
让脚本跑起来之前,多花两分钟让它"先试一个、先空跑一遍、先留好退路",这两分钟,往往能省掉后面一整场手忙脚乱。