
我电脑里曾经同时躺着五个“存代码片段”的地方一个 IDE 的本地片段库、一个笔记软件的代码块、一个专门存 shell 脚本的目录、GitHub Gist还有散落在各项目 README 底部的“历史遗留片段”。结果就是真到要用的时候我往往找不齐反而靠记忆重写一遍。t3code 就是在这种混乱里长出来的东西——一个终端下的本地代码片段管理工具。它不是什么大平台就是一个能用命令快速存取、检索、渲染片段的小程序核心诉求是“片段的组织、搜索和复用”这三件事尤其适合在终端里写脚本、维护配置、做运维和自动化的人。这篇文章是我对 t3code 整个设计和开发过程的完整复盘包括数据模型、存储方案、命令设计、同步策略以及我在实际使用中踩过的几个坑。1. 为什么会有 t3code从三个真实场景说起1.1 场景一同一段逻辑被重写了七次某个自动化脚本里需要解析一种特殊格式的日志行我记得半年前写过一段正则但既忘了存在哪个文件里也记不清具体写法。最后只能重新对着样例数据一遍遍试。这个场景在很多人身上都发生过最该被复用的东西往往最难被找到因为“找”这个动作的成本太高了高到不如重写。我当时试着用 grep 去各个目录里搜但能搜索的前提是你得记得“某个特殊字符串片段”——可问题恰恰就在于我连那个字符串长什么样都忘了。我需要的是一个能按标签、按描述、按模糊记忆检索的片段库而不是另一个文件系统。1.2 场景二跨设备同步靠 U 盘我有两台常用机器一台是办公台式机一台是笔记本。很多时候笔记本上调试好的 SQL 片段、shell 函数第二天到办公室又要重新整理一遍。最初我用 Git 仓库裸同步但问题在于一个片段仓库里塞进了太多不相干的内容提交历史一团乱麻时间长了根本不想去维护。这种“同步靠手动复制”的做法本质上不是技术问题而是组织问题——你没有给片段一个稳定的“家”。没有统一的数据格式、没有明确的目录结构、没有版本管理自然也谈不上跨设备。t3code 出现之后我把数据放在一个 XDG 标准目录下配合 Git 远程仓库做推送拉取整个过程才算稳定下来。1.3 场景三从笔记软件里复制代码总是变样我在笔记软件里存过不少代码。问题是贴进去的时候好好的复制出来的时候缩进被吃掉、引号被转义成中文全角符号、字体把空格显示成了看不见的东西。更麻烦的是这些软件启动慢从一个片段到真正粘贴进编辑器中间至少隔了五秒。这些需求积累到一定程度就不是“要不要做个管理工具”的问题了而是“再不做就疯了”。t3code 的雏形很简单一个 Go 写的命令行程序把片段存进 SQLite用命令行完成增删改查。后来慢慢加了标签、模板变量、加密字段、导出导入才变成现在这套东西。这个项目也从侧面说明一件事真正好用的工具通常不是为了“酷”而做的而是从一个让你烦躁了许多次的细节开始的。2. t3code 的定位与核心设计取舍2.1 t3code 到底是个什么工具t3code 是一个本地优先的、终端环境下的代码片段管理工具。它的数据默认只存在你机器上的一个目录里不对接任何云端。它解决的核心问题有三个收集用一条命令把片段存进来顺手配上标题、描述、标签不打断手头工作。检索通过标签过滤、全文搜索、模糊匹配快速定位到想要的片段。复用把片段渲染成真实可用的代码支持模板变量替换可以直接输出到 stdout 或剪贴板。它不会做的事也很明确不做语法高亮编辑器、不做云笔记、不做代码评审。因为这些都不是“终端下最痛的点”。一个工具想做好必须敢于放弃一些功能。现实是我见过太多项目因为想同时满足所有需求结果每个功能都平庸。t3code 的取舍原则就是只做好“存、找、用”这三个动作。2.2 命名里的小心思T3 的含义T3 这个代号不是随便起的。T 指 Terminal指这个工具的运行环境是终端3 的含义稍微复杂一点——首先是 Third generation因为这是第三个版本其次是 Three core actions也就是存、找、用三个核心动作。加在一起T3 就代表了“终端上的第三次迭代、三个核心操作”的意思。这个命名还有一个实际好处命令短。我把它装进系统后给 t3code 设置了一个 shell 别名scsnippet code 的缩写实际使用中几乎不需要打完整命令。工具的名字越短你越愿意用它。这听起来像废话但真实开发中很多工具失败的原因之一就是名字太长导致调用成本高。2.3 技术选型为什么用 Go 和 SQLite选 Go 的理由很简单编译出来是单一二进制文件不依赖运行时环境。我需要在两台不同配置的机器上部署Go 的 cross-compile 支持让我一条命令就能编译出 Linux 和 macOS 两个版本。相比 Python省去了“目标机器上有没有对应版本的解释器”这种问题相比 Rust开发速度更快足够应付这种体量的项目。数据库用 SQLite 而不是 JSON 文件是基于一个实际痛点片段一旦多了JSON 文件的每次读写都是全量操作时间成本会线性上升。SQLite 的 B 树索引和全文搜索能力在十万量级的记录下依然能毫秒响应。而且 Go 标准库里有现成的database/sql接口社区里成熟的 SQLite 驱动也不少集成成本并不高。还有一些小决策值得一提CLI 框架用 Cobra。它提供了标准的子命令结构、帮助信息、flags 解析省去我手写参数解析的时间。输出格式默认彩色输出但在非 TTY 环境下自动降级为纯文本。这个细节后面专门讲。配置格式TOML。比 YAML 少了很多缩进解析的坑比 JSON 更适合手写。2.4 核心概念片段、标签、别名t3code 的数据模型围绕三个实体展开实体说明实例Snippet一条完整的代码片段记录awk 提取日志中的 HTTP 状态码Tag分类标签支持层级shell/awk、sql/query、opsAlias片段的别名用于快速索引st-cod、status-code关系很简单一个片段可以有多个标签一个标签下可以有多个片段别名是片段的“快捷方式”输入别名能直接打开对应的片段。之所以把标签单独做成一张表而不是字段是因为我需要在“按标签做聚合统计”时有更好地性能。单独建表后一次GROUP BY就能算出哪些标签用得最频繁顺便还能发现哪些标签已经严重过时方便清理。3. 数据管理从目录规划到 SQLite 表结构3.1 数据目录遵守 XDG Base Directory 规范t3code 的所有数据存放在一个目录其路径通过以下优先级确定环境变量T3CODE_HOME指定的路径~/.config/t3code下的data子目录在 macOS 上使用~/Library/Application Support/t3code。选用 XDG 规范的最大好处是备份的时候只需要拷这一个目录卸载的时候删掉这一个目录不会有乱七八糟的残留文件散落在各个角落。目录内部的结构大致如下t3code/ ├── t3code.db # SQLite 主数据库 ├── config.toml # 配置文件 ├── templates/ # 自定义片段渲染模板可选 └── exports/ # 导出数据的临时目录3.2 SQLite 表结构设计表结构是 t3code 的重中之重。我最早的设计是把所有字段塞进一张大表用 JSON 存标签结果查询和统计都很别扭。后来重构成了下面这样CREATE TABLE snippets ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT DEFAULT , content TEXT NOT NULL, lang TEXT DEFAULT text, is_encrypted INTEGER DEFAULT 0, encrypted_content BLOB DEFAULT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now)), updated_at TEXT NOT NULL DEFAULT (datetime(now)) ); CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ); CREATE TABLE snippet_tags ( snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (snippet_id, tag_id) ); CREATE TABLE aliases ( id INTEGER PRIMARY KEY AUTOINCREMENT, snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE, alias TEXT NOT NULL UNIQUE ); CREATE TABLE meta ( key TEXT PRIMARY KEY, value TEXT );几个设计上的考虑is_encrypted和encrypted_content是配套的。普通片段明文存content敏感片段则把密文放encrypted_contentcontent字段留空。这样既不影响全文检索也不暴露敏感信息。snippet_tags表是典型的多对多关联表用级联删除保证数据一致性。meta表存一些元信息例如当前 schema 版本号方便日后做迁移。3.3 配置文件的优先级配置会从三个位置依次读取后面的覆盖前面的内置默认值config.toml中[defaults]段的全局配置每条命令运行时的 flag 参数。配置文件示例[defaults] lang bash editor vim pager less [search] case_sensitive false fuzzy_threshold 0.6 [security] secret_key_env T3CODE_KEY [render] prompt_vars true这个优先级设计是有意为之默认值保证程序在任何环境都能跑起来配置文件处理那些“大多数时候是这样但偶尔不是”的情况命令行 flag 则处理临时性的例外。比如我默认用 vim 编辑但偶尔想在某个片段上用 code 编辑器打开就直接加--editor code不影响全局配置。3.4 全文检索用 FTS5 解决“记不清关键词”的问题如果只靠LIKE %keyword%做搜索一方面是慢另一方面是没法处理复杂的匹配场景。SQLite 自带的 FTS5 扩展可以建立全文索引配合matchinfo()还能做简单的相关度排序。我为 snippets 表建立了一张虚拟表CREATE VIRTUAL TABLE snippets_fts USING fts5( title, description, content, contentsnippets, content_rowidid, tokenizeunicode61 remove_diacritics 2 );这里有个坑默认的unicode61tokenizer 对中文和代码符号的处理能力有限。比如它会把C识别成c和两个 token搜索C会匹配到很多不相干的结果。这个我在后面踩坑实录里专门讲。日常搜索会先用 FTS5 跑一遍再把命中的 id 集合去 snippets 表回表取完整数据SELECT s.id, s.title, s.description, s.lang, snippet(snippets_fts, 0, [, ], ..., 12) AS preview FROM snippets_fts JOIN snippets s ON s.id snippets_fts.rowid WHERE snippets_fts MATCH ? ORDER BY rank LIMIT 50;snippet()函数能直接生成带高亮标记的摘要省了我从原文里截取上下文的工作。4. 核心命令设计围绕“存、找、用”三个动作4.1 命令流转一条片段从进来到被使用的完整链路t3code 的核心命令一共六个构成一条完整的生命周期# 存入新片段 sc add --title 解析 Nginx 日志状态码 --lang bash --tags ops,nginx -f ./parse.sh # 检索片段模糊搜索 sc find nginx status # 查看单个片段的完整内容 sc get parse-nginx # 用默认编辑器修改片段 sc edit parse-nginx # 把片段内容渲染后复制到剪贴板 sc copy parse-nginx --set port8080 # 删除不再需要的片段 sc rm parse-nginx这里面最有意思的交互是sc get parse-nginx里的parse-nginx。它可以是片段 ID、唯一别名或者标题的一部分。t3code 的解析顺序是先查别名表命中就直接返回再查标题精确匹配最后才用 LIKE 做模糊匹配。这个顺序对应的是“确定知道”“半记得”“只记得大概”三种使用场景每一种都能在一步之内得到结果。4.2 模板变量让一个片段适配多种场景如果片段只是静态文本那它本质上就是一个“快速粘贴板”。t3code 加了模板变量的支持后片段才真正有了复用的价值。变量语法是{{ var_name }}同时支持简单的默认值和管道式格式化# 片段示例启动临时 Docker 容器 docker run -d --name {{ name | default: temp }} \ -p {{ port | default: 8080 }}:{{ port | default: 8080 }} \ -e ENV{{ env | default: dev }} \ {{ image }}使用的时候可以用--set批量传参sc run start-temp-docker --set namedbg1 port9090 envtest imagepostgres:16渲染逻辑本身不复杂先扫描出所有{{ ... }}占位符然后依次检查命令行参数、环境变量、配置文件默认值最后用内置的格式化函数处理输出。但就是这么一点功能把“存代码”变成了“存可复用的工具模板”同一个 SQL 查询模板可以跑不同的表名同一个部署命令模板可以传不同的环境同一个 shell 脚本片段可以接受不同的参数。4.3 标签组织不要设计完美分类而要设计可生长分类关于标签我原来犯过一个错误试图在开始阶段就把所有分类设计得完美。结果要么是分类太多导致存代码时要想半天该归到哪一类要么是分类太粗导致检索时命中的结果太多。最后我采用了一种务实策略标签数量不限但不允许嵌套太深最多两级标签名必须可以望文生义避免使用缩写定期运行sc tags --sort count找出最常使用的标签顺手清理低频标签。这个策略的好处是存代码时几乎不需要思考“分类学”只需要凭直觉打上两三个词即可。检索反而更精确因为多个标签的组合能大大收窄范围。4.4 与 Shell 联动把片段库变成日常命令的延伸光有命令行还不够t3code 的很多便利性是靠 shell 联动实现的。我在.bashrc/.zshrc里加了这样几行alias sct3code alias scft3code find | head -30 scr() { t3code get $1 --raw; } sccp() { t3code get $1 --set port${2:-8080} | pbcopy; }scr直接输出原始代码内容方便立刻放进管道里继续处理sccp则把渲染后的内容直接送进剪贴板。这个组合在我日常工作中的使用频率非常高——从写 SQL 到重启服务都是一条命令的事。这里想说的是一个终端工具如果不主动考虑和 shell 的配合它的使用成本就还是太高。哪怕只是减少敲几个字长期下来也能省下大量时间。这也是 t3code 宁可做得“小”也不做成一款 GUI 应用的原因。5. 同步与备份本地优先不等于孤立无援5.1 为什么我不直接上云同步开发早期有朋友建议我接一个云同步服务这样多设备自动同步岂不更好。但我拒绝了这个方案理由有两条一是“本地优先”的核心原则不能丢。数据只有一份默认副本在自己手里才是可控的一旦默认副本在云端离线、迁移、审查都是额外负担。二是同步冲突的复杂度远超想象。自动同步意味着要设计合并策略、冲突标记、重试机制、离线优先级这些工作量对于一个个人工具来说过重了。所以我选择了“手动触发、基于 Git 的推送拉取”同步模式# 推送本地所有数据到远程仓库 sc sync push # 拉取远程数据到本地 sc sync pull这个命令背后做的事情非常简单把数据目录里的 SQLite 数据库和配置文件统一提交到 Git 仓库然后 push 到远端。为了避免数据库文件在运行中变更导致提交损坏push 前会先用备份接口生成一个一致的快照。5.2 加密导出与导入有一种情况是我不想把整个 Git 仓库暴露给远程服务哪怕它是私有仓库但希望把某些片段带到别的机器上。t3code 为此提供了加密导出# 导出时用口令加密生成单一文件 sc export --encrypt --output snippets.t3e # 导入时输入相同口令 sc import snippets.t3e加密算法用的就是 Go 标准库的crypto/aes配合 GCM 模式秘钥从用户输入的口令通过scrypt派生。整个加密导出文件是一个自定义的二进制格式先写魔数、版本号再写加密后的 JSON 数据。这个格式不追求通用只求简单可靠。5.3 安全细节只有密码是不够的敏感片段的保护上t3code 做了三件事数据库中敏感字段以密文存储非敏感字段明文存储密钥不落盘而是从环境变量读取输出时默认不展示加密片段内容必须显式加--reveal参数。这个设计的核心思路是“分层防护”即使数据库文件被拷走了没有密钥就只能看到一堆不可读的密文。而密钥放在环境变量里而不是配置文件里是因为环境变量更不容易被误上传到版本库也更容易做到“不同终端用不同密钥”。5.4 与其他工具的连通导出 Markdown 和 JSONt3code 没有做封闭生态而是提供了两种导出格式方便我把它接到其他工作流里Markdown 导出每个片段生成一个.md文件适合直接放进文档库或知识库。JSON 导出全量数据导出为结构化 JSON适合做二次开发或迁移到别的工具。很久之后回头来看这个决定让 t3code 没有变成“又一座孤岛”。工具之间的数据应该是流动的哪怕只是一个简单的导出功能也能避免日后想换工具时被数据锁死。6. 踩坑实录三个让我重新设计的问题6.1 FTS5 对代码符号和中文的匹配问题最早我用默认的unicode61tokenizer 建 FTS 表很快就发现一个无语的现象搜索C时所有带字母c的片段都被捞出来了。原因是unicode61会把C拆成c和两个独立 token而且默认忽略单个字符的停用词c就变成高频噪音词了。这个问题在代码片段场景里尤其严重因为代码里充满了::、-、{、}等符号。FTS5 对符号的处理方式直接影响了检索质量。我最终的方案是构建 FTS 索引时额外把所有非字母数字的符号用统一的占位符_替换保证“带符号的代码文本”仍能被当作整体匹配在查询时对用户输入做同样的预处理保持索引端和查询端的一致性中文字符则依赖unicode61的 CJK 支持因为它是按单个汉字切分的对中文片段来说这个行为基本符合需求。实际效果是搜索docker run --rm时能精确命中相关片段不再被docker和run各自为政的行为干扰。6.2 终端宽度与彩色输出的兼容性做 CLI 工具最容易被低估的复杂度就是“终端环境怎么都不一样”。t3code 第一次在窄窗口上运行时彩色表格完全错位高亮文本被终端转义序列打断小窗口里一片混乱。这个问题还不仅仅是“宽度自适应”那么简单。我的处理思路分成几层输出前先请求终端的列数拿不到就默认 80 列对于超宽内容不截断而是折叠并保留完整的代码内容副本非 TTY 环境比如输出重定向到文件自动关闭 ANSI 颜色和交互式进度条所有输出在写入前都经过一个sanitizeOutput()函数确保终端转义序列永远不会意外执行。这个问题的教训是不要假设终端永远支持你习惯的特性。越接近“零依赖的工具”越要防御性处理这些边界条件。6.3 SQLite 的锁竞争与并发读写的坑t3code 本身是单用户的命令行工具按理说不该碰见并发问题。但有一次我写了一个 shell 脚本循环调用sc find做批量处理同时边缘又有一个sc sync push在写数据库结果频繁出现database is locked。SQLite 的默认 journal 模式是delete每次写操作都会短暂地持有排他锁当写操作尝试升级锁失败时就会立刻报错。对我这种场景正确的解法是使用 WAL 模式PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL;WAL 大大提高了读写并发性读操作不会阻塞写操作写操作之间也会用较小的临界区来做串行化。改成 WAL 之后再也没出现过database is locked。当然 WAL 也有代价比如数据库目录下会多出-wal和-shm文件备份时需要一并处理。我对备份命令专门做了处理同步的时候会先执行PRAGMA wal_checkpoint(TRUNCATE)再打快照。6.4 从坑里总结出的三个原则索引和查询必须使用同一套文本处理逻辑否则索引就是负资产输出格式要防御终端差异宁可朴素也不能错乱数据库配置要面向真实使用场景不要永远停在默认值上。这三个原则后来反过来重塑了 t3code 的整体设计搜索模块里所有文本处理函数被统一收口到一个包输出层单独抽了一层 renderer数据库连接时自动执行必要的 PRAGMA。这也是迭代开发中最有价值的部分——问题倒逼结构完善比一开始空想设计要靠谱得多。7. 我还会继续做的事t3code 目前已经在我的工作流里稳定运行了几个月每天都会新增或调用若干片段。从个人体验来说它最大价值不是“存了很多代码”而是让我越来越敢于放手写临时脚本——反正最终版本可以沉淀下来下次用到时直接调取。几个后续方向已经在计划里界面层加一个可以在终端里交互浏览的 TUI 模式检索支持更细粒度的语言过滤模板渲染支持调用外部命令做复杂格式化以及把加密导出格式规范化成可公开的稳定版本允许第三方工具读取。如果你也是“片段散落四处、经常重写相同逻辑”的人我建议你认真捣鼓一下类似 t3code 的小工具。它不一定需要多么强大的功能只要让“存”和“找”小于“重新写”的成本就已经值回票价了。最后分享一个我认为最重要的设计心得这样一个工具所有设计决策都应该围绕“我到底在什么场景下会用到它”来展开而不是围绕“它能做多少事”来展开。一个只被自己使用的工具真正做到顺手比做到强大重要得多。