如果你和我一样,每天要在浏览器、IDE、终端、聊天窗口之间搬运代码,你一定遇到过这种隐蔽的坑:从网页里复制一段代码,粘贴进文件后缩进全乱、行尾挂着 ^M、字符串里的中文引号全变了样。时间久了,我决定不再手动跟这些问题纠缠,花了小半年碎片时间做了个命令行小工具 t3code,专门把剪贴板里乱七八糟的文本规整成能直接运行的代码。下面我会把 t3code 从想法到落地、再到反复踩坑的完整过程复盘一遍,适合那些经常处理代码文本、想让复制粘贴这件事变得干净利落的人参考。
1. 复制粘贴这件“小”事,藏着多少隐形炸弹
1.1 三种最常见的“脏输入”
只要你的工作流里存在“复制一段内容,然后粘贴到另一个环境”这个动作,就会遇到脏输入。我复盘了自己半年多的经历,大概可以分成三类。
第一类是网页和博客里复制的代码。很多技术网站为了排版好看,会在代码外面包一层 markdown 围栏,偶尔还加了行号。你辛辛苦苦复制下来的内容,可能开头是三行 ```python,每行源码前面还挂着一个带竖线的序号。这种内容直接保存,轻则语法报错,重则脚本能跑但结果莫名其妙。
第二类是聊天工具和邮件里的代码。专门用于聊天、写作场景的输入法,会把半角引号自动变成智能引号,会把两个空格变成一个全角空格,甚至会在文本里插入肉眼看不见的零宽字符。这些字符在输入框里看起来毫无问题,一旦落入编译器或解释器,就是 illegal character 级别的报错。
第三类是 PDF、幻灯片或会议纪要里的代码。这类来源更麻烦,除了行号,还会混入页眉页脚、自动断行、甚至莫名其妙的 Unicode 编码字符。我曾经从一份幻灯片里复制一段配置命令,粘到终端后怎么执行都报错,后来才发现每行前面都带了一个不可见字符串,终端把整段内容当成一个命令在解析。
1.2 为什么手动整理不是长久之计
面对这些脏输入,大多数人的第一反应是“手动改一下就好了”。实际上,手动改有几个很现实的问题。
首先是不可见。BOM、零宽空格、行尾的 CR,这些字符在绝大多数编辑器里默认不显示,你肉眼看不见,但运行环境看得清清楚楚。你手动改的时候,根本不知道要改哪里,只能等报错之后一点点试,非常消耗耐心。
其次是无价值重复。每一段复制来的代码都要走一遍“去围栏、去行号、修引号、改缩进”的流程。这种事情做一次觉得没什么,做二十次就会意识到,这完全是机械劳动,完全可以交给工具来处理。而且工具永远不会漏掉某一行,不会手滑删掉一个分号。
最后是标准不统一。不同来源的脏习惯完全不一样,有的用 Tab 缩进,有的用两个空格,有的行尾是 CRLF。如果每次都是手动调整,你根本没法沉淀出一套属于自己项目的代码风格。只有把规则固化成配置,才可能让所有进入项目的代码都先过一道统一的规范化处理,后续的格式化工具也不会因为杂乱的输入而被带偏。
因此我想做一个这样的工具:输入是“任何地方复制来的文本”,输出是“符合项目规范的干净代码”,中间做三层处理,这就是 t3code 最初的雏形。
2. t3code 的设计骨架:一条命令、三段式管线
2.1 命名与定位
先解释一下名字。t3code 没有什么高深的含义,“t3”是当时随手定的组件号,真正想表达的是后半部分——code。它的定位不是代码格式化器,也不是代码生成器,而是文本进入文件之前的那道“安检门”。它能帮你处理换行、编码、缩进、围栏、行号这些字符层面的问题,但不碰语法、不做重构、不改变代码的逻辑语义。这个边界很重要,后面我会专门讲,正是因为守住了这个边界,它才能在每天的工作流里稳定跑很久。
实现语言我选了 Python,理由很朴素:正则表达式写起来顺手,argparse 可以省掉写命令行参数解析的功夫,标准库就能覆盖文件、编码、剪贴板调用等大部分场景。缺点是启动时间大概在 100ms 量级,但对于“粘贴前整理”这种场景,人是完全可以接受的。真要追求极致性能,可以后期用 Go 或 Rust 重写,但我个人觉得没必要,工具的首要指标是稳定和灵活,而不是毫秒级响应。
2.2 三种使用方式
第一种是管道模式,适合处理文件或配合其他命令:
cat mess.txt | t3code clean > ready.py第二种是剪贴板模式,这是我日常最常用的。复制网页代码后,直接运行:
t3code paste --file main.py它会读取系统剪贴板,经过完整管线处理后,把结果写入 main.py。不想直接落文件的话,也可以写回剪贴板,然后自己到编辑器里再粘贴:
t3code paste --to-clipboard第三种是模板模式,用于从代码片段生成脚手架:
t3code bake python script --name fetch_data.py这个模式不是必须的,但它让工具从“清理器”变成了“生成器”的入口,我会在第五章展开。
2.3 三段式管线:为什么顺序不能乱
t3code 的核心管线分为三个阶段,我习惯叫它们strip(归一化)、parse(结构化)、normalize(着陆)。一开始我把所有规则塞在一条正则链里,结果后加的规则经常打乱前面的处理结果,后来才分成三层。
第一层归一化处理的是字节和字符级别的脏数据:检测并剥离 BOM、把 CRLF/CR 统一转成 LF、把不同编码(UTF-8、UTF-16、GBK)规范成 UTF-8、移除零宽字符。这一层必须先做,因为如果你先把行号和围栏去掉,再处理换行符,正则匹配的样本就变了,容易匹配错。
第二层结构化处理的是“来源格式”留下的痕迹:去掉 markdown 围栏、剥离编辑器行号、去掉每行前缀的列表序号。这一层放在换行归一化之后,是因为行号剥离依赖“每一行有独立的开头”,如果行尾还带着 \r,正则写起来不仅别扭,还容易把 \r 遗漏在源码里。
第三层着陆处理的是“落盘规范”:统一缩进、删除行尾空格、压缩连续空行、保证文件末尾有换行符。这些规则和项目里的.editorconfig、prettier的职责有一部分重合,但 t3code 处理的是它们进场之前的问题。代码先进 t3code 整理一遍,再到编辑器插件和 CI 里的代码风格检查,两级分工明确,不会互相打架。
这样划分之后,每条规则都有了固定的处理阶段,加新规则时只需要想清楚它属于哪一层,回归测试也更好写。
3. 逐条规则拆解:功能、原理与边界
3.1 字符层:看不见的坏字符
字符层是 t3code 里最基础、也最容易被人忽略的部分,但恰恰是这些“看不见的字符”最容易酿成隐蔽事故。下面是我默认启用的几条规则,每条都给出了输入到输出的对比:
| 规则 | 处理前 | 处理后 | 用途 |
|---|---|---|---|
| BOM 剥离 | \ufeff#!/usr/bin/env python3 | #!/usr/bin/env python3 | 防止 shebang 失效、防止首行被当作文本 |
| CRLF 转 LF | line1\r\nline2\r\n | line1\nline2\n | 避免行尾出现 ^M,避免跨平台 diff 混乱 |
| 智能引号转直引号 | “hello”变成"hello" | "hello" | 避免编译器把全角引号当字符串边界 |
| 全角空格转半角 | print("a b") | print("a b") | 避免排版空格混入代码 |
| 零宽字符移除 | a\u200bb | ab | 避免从聊天工具复制时混入不可见字符 |
这几条规则看似简单,却有一些值得注意的边界。比如“智能引号转直引号”并不是无脑替换,因为有些源码里的中文注释、中文文案本来就是全角引号,不该动。我的策略是:先尝试用简单的词法判断,识别出看起来像字符串字面量的区域,如果目标字符在字符串内部就不替换,只有在字符串之外的非字面量区域才做转换。这样既不会把中文注释里的引号改坏,也不会破坏字符串里的真实内容。
3.2 结构层:围栏与行号识别
结构层处理的是“复制来源”留下的痕迹。
最常见的 markdown 围栏分两种。一种是被 ```python 包裹的完整代码块,处理时直接删掉首尾围栏,把语言标签转成一条注释放到文件头部,比如# source: python,这样既保留了来源信息,又不会污染语法。另一种是没有围栏、但每行带了行号的代码,常见于在线分享工具生成的富文本。行号格式大概是这样:12 | def main():或者12) def main():。
行号剥离是整个工具里最需要克制的地方,因为代码本身也可能以数字开头。我的判断逻辑有三条硬性约束:
- 整段文本中,必须有一个连续递增的行号序列,比例达到 90% 以上,才认为它是编辑器行号;
- 行号和代码之间必须有明确分隔符,比如竖线、冒号、右括号、点号;
- 只要有一行不符合“数字加分隔符加代码”的结构,整段不剥离,宁可不处理也不误删。
举个例子,如果复制下来的是:
1) import os 2) import sys 3) 4) def main(): 5) print("hello")t3code 会识别出这是带行号的代码,剥离后得到干净的源码。反过来,如果你在代码里写了12fps = 60这种单行带数字开头的内容,但前后行没有连续递增序列,整段就不会被误处理。
3.3 着陆层:缩进与空行
着陆层的目标只有一个:让整理出来的代码符合你项目的落盘规范。
其中最重要的就是缩进。不同项目差别很大,有的用两个空格,有的用四个空格,有的坚持用 Tab。t3code 默认把输入里混合的 Tab 和空格统一成四个空格,理由很朴素:现在的编辑器里,四个空格是最常见的选择,也没有歧义。如果你项目本身用 Tab 或者两个空格,可以在项目级的.t3code.toml里覆盖:
[t3code] indent = "tab" # 可选:4spaces、tab、2spaces max_blank_lines = 1 # 连续空行最多保留几行 ensure_final_newline = true convert_smart_quotes = true strip_markdown_fence = true空行处理也值得说一下。很多人复制代码时,空行处会留下几个空格,肉眼根本看不见。t3code 解析完每一行之后,会把行尾空白全部删掉,再把连续空行压缩成最多一行。文件末尾统一补上一个换行符,这也是 Linux/Unix 工具链下最安全的选择,避免很多命令在拼接文件时出现最后一行粘连的问题。
这个阶段的通用原则是:所有落盘相关的规则都可以配置,但默认值一定是“最主流、最安全”的。因为工具是给别人用的,默认把特殊情况开关打开,不如让大多数场景贴着一路默认跑通。
4. 真实踩坑记录:从 BOM 到 heredoc 的连环事故
这一章不是功能说明书,而是我实际调试过程中踩到的坑。记录这些主要是为了说明一件事:做文本整理工具,很多问题不是规则本身写错,而是规则之间互相作用产生了意外。
4.1 坑一:BOM 让 shebang 静默失效
先说一个让我印象最深的坑。有一次我从一个在线编辑器的“导出代码”页面复制了一段 Python 脚本,保存后直接./run.py,结果终端报-bash: ./run.py: cannot execute: required file not found。当时第一反应是权限问题,检查了ls -l也没问题,又以为是 interpreter 路径写错,折腾了十几分钟,最后用xxd run.py | head -1一看,文件头赫然多了一个ef bb bf,也就是 UTF-8 的 BOM。
BOM 对很多代码文件来说是个隐形杀手:#!/usr/bin/env python3前面多出三个字节,内核在解析 shebang 时找不到解释器路径,于是脚本直接无法执行。更坑的是,不少编辑器里根本看不到这个字符,你看着源码完全正常,运行时就是不行。修复之后,我把“任何进入 t3code 管线的文本,第一步必须剥掉 BOM”写成了铁律。后来不管是复制还是导入文件,只要文本流里出现 BOM,一律在最前面清掉。
这个坑的排查过程让我重新认识到一个道理:文本处理工具的规则顺序往往比规则本身更重要,字符层必须优先于结构层。
4.2 坑二:智能引号误伤代码字符串
刚开始写引号转换规则的时候,我只简单地做了一步:把全角双引号“”替换成半角",把全角单引号‘’替换成半角'。结果没跑多久就出了新问题。有同事从聊天工具里复制了一段含中文注释的演示代码,注释里本来写的是“按下确认键”,结果被自动改成"按下确认键",虽然不影响运行,但看着很别扭。更严重的是另一段代码,字符串里故意存了一个全角引号用来做文本排版,也被规则冲掉了。
后来我把引号转换改成了带上下文的替换。先用一个最简易的字符串字面量扫描器,把代码中疑似字符串的部分标记出来,对于字符串内部的引号一律跳过;只有确定在字符串之外、看起来是从聊天工具或网页里带过来的全角引号,才做转换。这个扫描器不追求像真实词法器那样精确,只要能避免大部分误伤就行。验证下来,误报率明显下降。
这里也让我意识到,t3code 这类工具最怕的事情就是“过度聪明”。做全局替换之前,先问一句:这个规则会误伤真实代码吗?会,就不做默认开启。
4.3 坑三:剪贴板工具跨平台不一致
t3code 的paste模式一开始只在 macOS 上跑,用的自然是pbpaste。后来把它拿到 Linux 上,发现pbpaste根本不存在。Linux 下的剪贴板本来就分裂,X11 上用xclip,Wayland 上用wl-clipboard,而且这些工具在 SSH 会话里完全不可用,因为远程机器根本没有图形剪贴板。
解决方案是把剪贴板访问封装成一个 adapter,按环境自动选择:macOS 用pbpaste/pbcopy,Linux 上优先检测wl-copy/wl-paste,再退回xclip/xsel,实在都没有,就降级成从临时文件读写,并明确告诉用户“当前没有检测到可用的剪贴板工具,已使用临时文件模式”。这个降级看起来简单,但避免了在 SSH 场景下工具直接崩溃的问题。
如果你也做剪贴板相关的工具,我建议先把目标环境列一遍:macOS、Windows(WSL 或 Cygwin 下的 win32yank)、Linux 桌面、远程服务器。每个环境差异都很大,适配层的代码量可能比核心管线还多,但这是跨平台工具绕不开的。
4.4 坑四:heredoc 与 $ 符号
有段时间有用户反馈,从文档里复制了一段 shell 脚本,处理后$HOME变成了空字符串。排查发现,我的管线下游加了一个“自动修正变量转义”的模块,它在看到$HOME时会尝试把$转义成\$,本意是防止 heredoc 场景下变量被展开。但这个“好心”直接把普通 shell 脚本里的变量引用全部破坏了。
这个教训非常深刻:工具不该猜测内容的语义。heredoc 里$是否要展开,取决于使用场景,有些时候用户就是希望被 shell 展开,有些时候希望字面保留。工具无法从一段孤立文本里判断出这个意图,强行判断就是帮倒忙。从那以后,t3code 明确了一条边界:只处理格式和可见字符,不做任何语义推断。$、变量、转义、表达式这些内容,一律原样保留。格式的归格式,语义的归编译器。
4.5 坑五:行号剥离的误判
前文提到行号剥离有硬性约束,但这是经过一次误判事故才补上的。
最早版本的行号规则非常简单,看到行首是“数字 + 点”就剥离。结果有一次处理一段 Python 列表:
1) items = [1.5, 2.5, 3.5] 2) print(items)第一行的1.5被当成“序号 1”剥成了5,代码直接被改坏。因为第一行是列表项且以数字开头,第二行有真实的序号,整体看起来真的很像带行号的代码。之后我加入“连续递增 + 分隔符 + 全段一致”的约束,才基本规避这类问题。
现在行号剥离采用的思路是:先按行切分,如果超过 90% 的行都符合“数字 + 分隔符 + 代码”的结构,才把这些前缀整体裁掉;否则一律当作代码本身保留。这种“宁可少做,不可误删”的策略,对文本清洗工具来说永远是第一位的。
5. 从个人脚本到团队基建:扩展、测试与沉淀
5.1 黄金文件:让规则越改越稳
工具刚开始只有我一个人用,规则可以随便加。后来让同事也接入之后,一个很现实的问题摆在面前:怎么保证新增规则不会把之前处理好的代码再弄坏。
我的做法是给 t3code 建立了一组“黄金文件”测试样本,英文惯例叫 golden files。每个样本对应一条或一组规则,目录结构类似:
tests/golden/ markdown_fence.in.txt markdown_fence.out.txt line_number.in.txt line_number.out.txt smart_quote.in.txt smart_quote.out.txt每增加一条规则,就先补一对输入输出样本,再跑全量快照测试。只要某个样本的输出和预期不一致,就说明新规则影响了旧行为,需要立刻确认是优化还是破坏。这个测试成本非常低,但换来的是我可以放心地对规则做增量迭代,不用担心某个深夜加的正则把白天的正常处理搞崩。
5.2 加一个 --check 模式变成代码卫生检查
t3code 的规则本质上是一套“文本规范”。除了在写入文件前做自动清理,我把这套规范复用成了t3code check --file xxx模式:不修改文件,只报告发现的问题,例如 BOM、行尾空白、混合换行、连续空行过多、文件末尾缺换行。退出码非 0 时,就可以直接接到 CI 或pre-commit钩子里:
pre-commit run t3code --files src/这等于把“粘贴时自动清理”和“提交前自动检查”打通了。团队的代码风格不再依赖某个人的 EditorConfig 是否生效,而是无论代码来自哪里,最终落到仓库前都会过一遍同样的规则。实际接入之后,同事最大的感受是“很多以前要等 CR 时才能发现的格式问题,在保存文件时就被拦下了”。
5.3 hook 机制与模板生成
把静态的文本规整做完之后,我发现还能再做一层有界的动态处理,于是加了 hook 机制。
after-clean钩子会在 t3code 完成管线处理后、落盘前触发,允许你调用外部命令。比如我配置过一个简单的日志脱敏脚本,它可以自动把复制进来的配置片段里的 access key 打码,再输出到仓库。这个功能目前仍然被我刻意限制在“调用外部命令”这一层,没有内置任何脚本语言,避免工具本身膨胀成一个复杂平台。
另一个相关能力是模板生成。写t3code bake python script --name fetch_data.py时,t3code 会读取我预设的 Jinja 模板,把整理好的代码片段填进去,再按照命名规范检查输出文件名。这个方向其实很有意思:它把“清洗工具”变成了“脚手架入口”,同一个工具链既能清理脏文本,也能生成规范文件,工作流非常顺。
5.4 未来扩展:克制地做加法
工具做久了会有一个冲动:把所有能力都塞进来。我的原则是克制地做加法。目前考虑过的方向里,有三个相对明确。
第一个是 IDE 粘贴监听。在 VS Code 或 JetBrains 系编辑器里装个插件,粘贴时自动调用 t3code 管线,把格式化动作从“手动执行命令”变成“粘贴即完成”。
第二个是选区处理。很多时候需要的不是整段清理,而是只整理用户选中的几行,这要求工具能识别输入范围并保留上下文。
第三个是作为本地服务常驻。把管线封装成一个小型 HTTP 接口,供编辑器插件、CI 脚本甚至团队内部的网页工具统一调用,避免每个接入方都自己复制一份规则。
这些扩展里,前两个更贴近日常,第三个适合团队级别使用。但我不打算一次全部做完,因为 t3code 的价值核心从来不是功能多少,而是在你每天高频搬运代码时,能稳定、低调地把问题挡在门外。
维护 t3code 这半年,我最大的体会不是正则写得越来越熟练,而是“工具要长在真实场景里”。最初我想做一个全能型代码整理器,差点把它做成一个什么都管的大杂烩。后来砍掉所有语义猜测、只保留字符和格式层面的整理,它才真正变得可靠。如果你也经常要从各种奇怪的地方复制代码,建议你先别急着找大而全的插件,把自己最常遇到的三种脏输入写下来,做成一条固定规则链。哪怕只是一个几百行的脚本,只要能长期用、不误伤,就已经是个好工具了。t3code 目前依然只是我维护的一个小工具,但它在每个复制粘贴的瞬间替我挡住的问题,足够让我觉得这件事做得值。