命令行文本片段管理小工具的設(shè)計(jì)與實(shí)現(xiàn))
cua 是我最近做的命令行小工具名字來自我敲鍵盤時(shí)冒出來的第一感覺——短、響、干脆像有人把抽屜拉開又合上的那一聲。它解決的問題很小但特別煩人代碼里那些反復(fù)出現(xiàn)的片段為什么每次都要重新去翻數(shù)據(jù)庫連接串、正則表達(dá)式、部署命令、git 提交信息模板這些內(nèi)容我明明存過卻經(jīng)常要花幾十分鐘重新搜索一遍。于是我用一個(gè)周末寫了 cua專門做一件事把常用的文本片段快速存下來再更快地復(fù)制回去。這篇文章記錄需求、設(shè)計(jì)、實(shí)現(xiàn)和踩坑的完整過程關(guān)鍵代碼和取舍理由都放在下面。如果你也想給自己做一個(gè)十分鐘能跑通的效率工具可以拿它當(dāng)參考。1. 為什么我會(huì)被一個(gè)三字母命令套牢開局痛點(diǎn)與命名由來1.1 這個(gè)場(chǎng)景你們多半也遇到過大概在半年多前我接到一個(gè)維護(hù)老服務(wù)的活兒。那次排查問題需要在服務(wù)器上拼一串參數(shù)復(fù)雜的啟動(dòng)命令里面包含數(shù)據(jù)庫地址、緩存節(jié)點(diǎn)、日志路徑還有一個(gè)帶轉(zhuǎn)義的正則表達(dá)式。我先是在搜索引擎里搜了一圈看到好幾篇互相矛盾的文檔又去翻自己本地那個(gè)叫 snippets.md 的筆記文件文件已經(jīng)積累了上百條內(nèi)容CtrlF 翻了幾輪才找到一段半年前的記錄復(fù)制粘貼之后發(fā)現(xiàn)里面還混著行號(hào)和多余的換行導(dǎo)致命令直接執(zhí)行失敗。那一刻我就意識(shí)到問題不在于我懶而在于“存”和“取”這兩個(gè)動(dòng)作被嚴(yán)重割裂。筆記軟件適合寫長文章不適合放零散代碼片段聊天記錄里的代碼可以用但前提是你還能記得是哪一天、哪個(gè)群新建文檔就更不現(xiàn)實(shí)我總不能為了存一個(gè)正則表達(dá)式新建一個(gè)頁面。我想要的是這樣一類工具操作足夠快快到可以讓我愿意為一條三行代碼付出哪怕五秒鐘的保存時(shí)間查找足夠快快到讓我不再依賴瀏覽器的搜索歷史。后來我嘗試過幾個(gè)現(xiàn)成的片段管理軟件要么太重要裝客戶端、賬號(hào)、同步服務(wù)要么太輕只是把所有的文本塞進(jìn)一個(gè)大文件連最基本的按名稱搜索都做不好。于是我開始認(rèn)真考慮自己寫一個(gè)。核心需求其實(shí)就三條第一能用命令行直接操作因?yàn)閷懘a的時(shí)候我已經(jīng)在終端里了第二數(shù)據(jù)必須是我能看懂的文件不能是某個(gè)私有格式的數(shù)據(jù)庫第三復(fù)制結(jié)果要進(jìn)系統(tǒng)剪貼板而不是讓我再手動(dòng)選中一遍。1.2 cua 這個(gè)名字是怎么來的項(xiàng)目一開始的代號(hào)是 “snippet-cli”名字太長打起來也累在終端里敲了兩天就覺得煩。有一天我在想這個(gè)工具的核心動(dòng)作其實(shí)就是“把片段從口袋里掏出來”那一瞬間腦子里蹦出一個(gè)擬聲詞 cua像抽屜開關(guān)的聲音也像按鍵按下的聲音。我試著在終端里敲了三個(gè)字母確認(rèn)手感和節(jié)奏都好就決定用它。當(dāng)然為了在跟同事介紹時(shí)不至于被追問“cua 到底是什么意思”我后來湊了一個(gè)還算能講得通的展開Clipboard Utility for All即面向所有人的剪貼板工具。但說實(shí)話這個(gè)名字是事后硬湊的真正的原因就是短、好記、不容易撞名。在項(xiàng)目啟動(dòng)前我特意執(zhí)行了一下which cua確認(rèn)系統(tǒng)里沒有同名命令然后就把這個(gè)名字焊死了。這里也提個(gè)建議給工具取名一定要先查一次命令是否被占用避免安裝到你電腦上時(shí)和系統(tǒng)里的程序沖突。1.3 邊界它不做什么cua 的定位不是筆記軟件也不是密碼管理器更不是 TODO 管理。它的邊界非常清楚只處理純文本片段不做富文本不搞標(biāo)簽系統(tǒng)不為每個(gè)片段維護(hù)標(biāo)題、作者、創(chuàng)建時(shí)間這些元數(shù)據(jù)。原因也很直接任何額外的概念都會(huì)增加使用者的決策成本。筆記軟件要你思考該建哪個(gè)筆記本、打哪些標(biāo)簽而 cua 把決策壓縮為一步給你想存的內(nèi)容起一個(gè)文件名字存進(jìn)去完事。我見過不少同類項(xiàng)目最后變得難用都是因?yàn)楣δ茉郊釉蕉嘀С至藞D片、支持了 Markdown 渲染、支持了團(tuán)隊(duì)共享結(jié)果用戶存一個(gè)片段要考慮的事情比寫代碼本身還多。所以我在寫 cua 的時(shí)候給自己立了一條規(guī)矩任何功能如果不能在我按下回車之后的五秒鐘內(nèi)感受到價(jià)值就先不做。后面所有的實(shí)現(xiàn)包括存儲(chǔ)、匹配、復(fù)制全都是圍繞這條規(guī)矩展開的。2. 片段即文件cua 的存儲(chǔ)模型與設(shè)計(jì)取舍2.1 為什么不選數(shù)據(jù)庫一個(gè)“偷懶”的決策最開始我以為該用 SQLite畢竟片段管理天然適合結(jié)構(gòu)化存儲(chǔ)可以做標(biāo)簽、做全文索引、統(tǒng)計(jì)使用頻率。但仔細(xì)想了一圈之后我放棄了數(shù)據(jù)庫回到最原始的方式每個(gè)片段就是一個(gè)獨(dú)立的純文本文件。這個(gè)決策看起來“偷懶”實(shí)際算下來是最省事的方案。原因有幾點(diǎn)。首先是規(guī)模一個(gè)普通開發(fā)者的常用片段撐死也就幾百條這個(gè)規(guī)模用文件系統(tǒng)完全沒壓力根本不需要數(shù)據(jù)庫的索引能力。其次是可遷移性文本文件放哪兒都認(rèn)得用 U 盤拷走、用專門工具同步、甚至是打包發(fā)到另一臺(tái)機(jī)器都不會(huì)遇到格式問題。第三是生態(tài)文件存好之后我可以用 ripgrep、grep、find、編輯器自帶的全局搜索去翻它們這意味著 cua 哪怕有一天本身掛了我的數(shù)據(jù)也永遠(yuǎn)可以用基礎(chǔ)工具讀到。對(duì)比項(xiàng)純文本文件SQLite 數(shù)據(jù)庫初始化成本零一個(gè)目錄搞定需要建表、寫連接邏輯備份/版本管理git、壓縮包均可需要導(dǎo)出為其他格式檢索能力依賴文件名和全文掃描自帶索引適合超大片段庫查看便利性任何編輯器都能直接看需要命令行或圖形工具適用規(guī)模幾百條以內(nèi)上萬條、需要復(fù)雜查詢時(shí)后來實(shí)際跑起來也證明這個(gè)選擇帶來的額外紅利是我可以很方便地在片段目錄里執(zhí)行g(shù)it init把整個(gè)片段庫納入版本管理。這樣即使某一次批量修改出了問題也能回滾到上一份快照。如果你準(zhǔn)備做類似的工具我的建議是不要急著引入數(shù)據(jù)庫先想想你的數(shù)據(jù)規(guī)模到底有多大。2.2 文件名即標(biāo)簽cua 的片段存儲(chǔ)目錄默認(rèn)是~/.cua/snippets/里面允許再建一層子目錄作為分類。我的實(shí)際目錄結(jié)構(gòu)長這樣~/.cua/ snippets/ deploy/ docker-compose-postgres.yml nginx-ssl-conf.txt python/ regex-uuid.txt sqlalchemy-async-session.txt misc/ ssh-tunnel.txt config.toml每個(gè)文件的文件名就是它的標(biāo)簽。文件名的格式我強(qiáng)制推薦用 kebab-case也就是全小寫、單詞之間用短橫線連接例如docker-compose-postgres.yml。為什么不用空格或下劃線因?yàn)榭崭裨诮K端世界里會(huì)帶來無窮無盡的引號(hào)問題下劃線在模糊匹配時(shí)又不如短橫線容易拆詞。cua 在add的時(shí)候會(huì)自動(dòng)把用戶輸入的名稱做規(guī)范化處理空格、大寫、特殊符號(hào)都會(huì)被轉(zhuǎn)成短橫線這樣我手動(dòng)創(chuàng)建的片段無論多隨意最終落到磁盤上的文件名一定符合規(guī)則。分類目錄我控制在兩層以內(nèi)一層是分類名一層是文件名。層級(jí)做得越深使用者的心理負(fù)擔(dān)就越重最后的結(jié)果往往是懶得分類、把東西亂丟。如果你只有幾十個(gè)片段我建議干脆連分類目錄都省略全部平鋪在 snippets 目錄下靠文件名把含義表達(dá)清楚就夠了。2.3 片段內(nèi)容的格式約定片段的正文就是純文本。不過為了在列表展示和實(shí)際復(fù)制之間做區(qū)分我約定了一個(gè)非常輕量的規(guī)則如果文件的第一行以#開頭那么這一行被視為描述信息在list命令里顯示但不會(huì)被復(fù)制到剪貼板。比如一個(gè) shell 配置片段# 生成安全的隨機(jī)令牌 openssl rand -base64 32當(dāng)cua list展示時(shí)你會(huì)看到一行“生成安全的隨機(jī)令牌”這時(shí)你一眼就知道這段內(nèi)容是干嘛的當(dāng)cua copy執(zhí)行時(shí)它只會(huì)復(fù)制第二行的實(shí)際命令描述行會(huì)被自動(dòng)剝離。如果某個(gè)片段本身就是一段以#開頭的代碼比如 Python 注釋、Shell 注釋那它也只會(huì)影響列表顯示不影響復(fù)制結(jié)果因?yàn)閺?fù)制時(shí)剝離規(guī)則只判斷第一行。實(shí)際上這個(gè)小約定是我在寫筆記時(shí)順手加上的。早期版本會(huì)把描述和正文一起復(fù)制進(jìn)剪貼板結(jié)果我粘貼到終端里總要多刪一行非常惱火。后來加了剝離邏輯這個(gè)問題就徹底消失了。你也可以理解為cua 把每個(gè)片段文件都當(dāng)成一個(gè)極簡(jiǎn)的“標(biāo)題正文”結(jié)構(gòu)標(biāo)題來自文件名描述來自第一行注釋剩下的全是內(nèi)容。3. 從 stdin 到剪貼板的完整鏈路核心命令與關(guān)鍵實(shí)現(xiàn)3.1 存儲(chǔ)目錄解析與測(cè)試友好性cua 是用 Python 3.9 寫的主要理由是不需要編譯、跨平臺(tái)行為一致而且標(biāo)準(zhǔn)庫就能完成大部分工作。依賴只有一個(gè) pyperclip 用來讀寫系統(tǒng)剪貼板列表渲染用的 rich 是可選項(xiàng)。核心代碼的第一步是確定存儲(chǔ)根目錄我讓它優(yōu)先讀取環(huán)境變量CUA_HOME沒有設(shè)置時(shí)才落到~/.cuaimport os from pathlib import Path def get_store() - Path: root Path(os.environ.get(CUA_HOME, Path.home() / .cua)) snippets root / snippets snippets.mkdir(parentsTrue, exist_okTrue) return snippets這里特別說一下為什么要做CUA_HOME這個(gè)環(huán)境變量。如果所有路徑都寫死成~/.cua測(cè)試時(shí)會(huì)污染真實(shí)數(shù)據(jù)而且每次跑測(cè)試都要想辦法清理有了環(huán)境變量測(cè)試?yán)镏灰阉赶蛞粋€(gè)臨時(shí)目錄再往臨時(shí)目錄里寫文件測(cè)試結(jié)束后自動(dòng)銷毀互不干擾。這也是一個(gè)可以復(fù)制到其他小工具里的通用設(shè)計(jì)凡是會(huì)在磁盤上留下數(shù)據(jù)的程序都應(yīng)該允許用戶通過環(huán)境變量或參數(shù)指定數(shù)據(jù)目錄。3.2 add 命令從管道和剪貼板兩種方式取數(shù)據(jù)cua add的目標(biāo)是讓“保存一個(gè)片段”的操作時(shí)間壓縮到三秒以內(nèi)。設(shè)計(jì)上它支持兩種數(shù)據(jù)來源如果終端有標(biāo)準(zhǔn)輸入正在往管道里傳內(nèi)容就讀取標(biāo)準(zhǔn)輸入否則就讀取系統(tǒng)剪貼板。具體邏輯是這樣的import sys def read_payload(): if not sys.stdin.isatty(): return sys.stdin.read() import pyperclip text pyperclip.paste() if not text.strip(): raise SystemExit(error: stdin is empty and clipboard is empty) return text這個(gè)邏輯解決了一個(gè)問題在用cat查看某個(gè)文件、或者在瀏覽器里復(fù)制了一串代碼之后我可以立刻切回終端執(zhí)行cua add docker-compose-postgres它會(huì)把剪貼板里的內(nèi)容變成一個(gè)新片段不用再打開編輯器粘貼保存。習(xí)慣之后保存一個(gè)片段的成本幾乎可以忽略不計(jì)。如果你在編寫類似的工具我建議一定要支持 stdin因?yàn)檫@種無意識(shí)的零成本保存才是你愿意長期堅(jiān)持使用的前提。對(duì)于已經(jīng)存在的同名片段默認(rèn)行為是直接覆蓋覆蓋前打印一行警告。這個(gè)設(shè)計(jì)一開始遭到我自己的懷疑生怕誤刪內(nèi)容但真實(shí)使用中發(fā)現(xiàn)同一名稱的片段往往就是同一類內(nèi)容的迭代版本覆蓋帶來的收益大于風(fēng)險(xiǎn)。如果你希望嚴(yán)格模式可以加一個(gè)全局參數(shù)--no-clobber遇到同名文件時(shí)直接報(bào)錯(cuò)退出。3.3 grab 的模糊匹配思路cua 的查找命令有兩個(gè)cua list用于瀏覽全部cua grab用于按關(guān)鍵詞快速找到片段。grab 是我用得最多、也是實(shí)現(xiàn)時(shí)最講究的命令。它的核心思路是把用戶的查詢?cè)~轉(zhuǎn)成一個(gè)“按順序匹配字符”的正則表達(dá)式比如輸入pgconn它需要能匹配到文件pg-conn-string.txt。實(shí)現(xiàn)里我做了一個(gè)叫 compile_fuzzy 的函數(shù)它會(huì)忽略掉短橫線和下劃線把用戶輸入的每個(gè)字符看作必須按順序出現(xiàn)的線索import re def compile_fuzzy(query: str) - re.Pattern: compact_query query.replace(-, ).replace(_, ).lower() parts [] for i, ch in enumerate(compact_query): if i 0: parts.append(r.*?) parts.append(re.escape(ch)) return re.compile(.join(parts), re.IGNORECASE)匹配時(shí)不但檢查原始文件名也檢查去掉短橫線之后的緊湊版本所以pgconn能命中pg-conn-string.txtsqlalchemy也能命中帶分類前綴的長文件名。如果你用過編輯器里的模糊查找就會(huì)覺得這個(gè)體驗(yàn)很自然不需要記全名不需要管分隔符只要記住幾個(gè)關(guān)鍵字母就夠了。很多人以為模糊匹配很難其實(shí)在片段文件名這種短文本上一個(gè)如此簡(jiǎn)單的正則就夠用了完全沒有必要引入復(fù)雜的編輯距離算法。3.4 copy 和 edit高頻操作要快找到片段之后最關(guān)鍵的動(dòng)作就是復(fù)制。cua copy key會(huì)先走與 grab 相同的匹配邏輯然后讀取文件內(nèi)容、剝離描述行、寫入系統(tǒng)剪貼板。這里的核心代碼非常短def cmd_copy(name: str): store get_store() matches search_files(store, name) if not matches: raise SystemExit(ferror: no snippet matched: {name}) snippet load_snippet(store, matches[0]) import pyperclip pyperclip.copy(snippet[body]) print(fcopied {matches[0].name} to clipboard)edit命令則負(fù)責(zé)打開編輯器修改片段內(nèi)容。它會(huì)優(yōu)先使用$EDITOR環(huán)境變量指定的編輯器默認(rèn)回退到viimport subprocess def cmd_edit(name: str): store get_store() matches search_files(store, name) if not matches: raise SystemExit(ferror: no snippet matched: {name}) editor os.environ.get(EDITOR, vi) subprocess.run([editor, str(matches[0])])這里有個(gè)細(xì)節(jié)編輯完成后我沒有做任何文件變更檢測(cè)因?yàn)榫庉嬈鞅4婧髢?nèi)容自然落在文件里后續(xù)再 grab 或 copy 時(shí)就會(huì)讀到新內(nèi)容。這種設(shè)計(jì)讓 edit 命令變得非常簡(jiǎn)單也符合 Unix 工具的哲學(xué)程序只負(fù)責(zé)找到文件并打開保存由編輯器負(fù)責(zé)。如果你要學(xué)習(xí)這個(gè)項(xiàng)目的代碼建議從這三條命令開始讀它們各自解決了存取鏈路的一個(gè)環(huán)節(jié)。3.5 一個(gè)最小測(cè)試矩陣為了確保這些命令在改動(dòng)后不會(huì)壞掉我給 cua 寫了一套極簡(jiǎn)的測(cè)試核心就是利用CUA_HOME臨時(shí)目錄。下面這段測(cè)試覆蓋了最常用的 add 和 grab 鏈路import importlib.util import io import os import sys import tempfile from pathlib import Path def test_add_and_grab(): with tempfile.TemporaryDirectory() as tmp: os.environ[CUA_HOME] tmp old_stdin sys.stdin sys.stdin io.StringIO(hello world) try: cua importlib.import_module(cua) cua.call([add, hello]) match cua.search_files(cua.get_store(), hello) assert len(list(match)) 1 body cua.load_snippet(cua.get_store(), list(match)[0])[body] assert body hello world finally: sys.stdin old_stdin測(cè)試并不復(fù)雜但它能保證最基本的添加、文件名規(guī)范化和搜索邏輯在重構(gòu)后仍然可用。對(duì)于個(gè)人項(xiàng)目來說這已經(jīng)足夠讓我安心地隨意修改代碼而不用擔(dān)心哪一次順手刪掉了某個(gè)功能。如果你嫌寫測(cè)試麻煩至少也要保證每個(gè)命令在干凈臨時(shí)目錄里能打出 help 信息。4. 真實(shí)使用中才會(huì)撞上的四個(gè)坑編碼、遠(yuǎn)程、空格與同步4.1 名稱里的空格終端世界的隱形炸彈第一個(gè)坑是在我用了大概一周之后踩中的。某次我想存一個(gè) docker 命令片段順手在終端里執(zhí)行了cua add docker compose up -d。結(jié)果它把“docker compose up -d”這一整串空格都保留成了文件名的一部分于是磁盤上出現(xiàn)了一個(gè)名字里帶四個(gè)空格的怪異文件。接下來所有跟它相關(guān)的操作都要打引號(hào)列表里顯示也對(duì)不齊最后我只能手動(dòng)去目錄里重命名。為了解決這個(gè)問題我在 add 命令里加入了強(qiáng)制規(guī)范化無論用戶輸入什么名字都會(huì)先轉(zhuǎn)為小寫然后把連續(xù)空格轉(zhuǎn)成短橫線再過濾掉除字母、數(shù)字、短橫線、點(diǎn)號(hào)之外的字符。所以上面那條命令實(shí)際上會(huì)存成docker-compose-up-d.txt而這個(gè)文件里存的內(nèi)容是“docker compose up -d”這條命令本身。規(guī)范化的規(guī)則也在 grab 返回結(jié)果時(shí)采用保證兩邊邏輯一致。這條經(jīng)驗(yàn)讓我明白在命令行工具里寧可替用戶做一點(diǎn)看似武斷的決策也不要讓用戶為隨后的引號(hào)問題買單。4.2 編碼問題Windows 和舊終端下的中文亂碼第二個(gè)坑是編碼。cua 最初在 macOS 上跑得很順但換到一臺(tái) Windows 機(jī)器上之后凡是包含中文的片段保存后再讀取全都變成了亂碼。原因很簡(jiǎn)單Python 在 Windows 上讀寫文本文件時(shí)默認(rèn)編碼可能是系統(tǒng)區(qū)域設(shè)置對(duì)應(yīng)的編碼而不是 UTF-8。中文系統(tǒng)下常見的是 GBK用 GBK 寫入、再被其他工具按 UTF-8 讀取自然就亂了。解決辦法是在所有文件讀寫操作里顯式指定編碼為 UTF-8并且在讀取時(shí)容忍無法解碼的字節(jié)def read_text(path: Path) - str: return path.read_text(encodingutf-8, errorsreplace) def write_text(path: Path, content: str) - None: path.write_text(content, encodingutf-8)errorsreplace的意思是遇到無法識(shí)別的字節(jié)時(shí)不要拋出異常而是替換成占位字符這樣至少保證不會(huì)因?yàn)橐粋€(gè)壞字節(jié)導(dǎo)致整個(gè)命令崩掉。另外在 Windows 終端里顯示中文時(shí)建議設(shè)置環(huán)境變量PYTHONIOENCODINGutf-8讓標(biāo)準(zhǔn)輸出也使用 UTF-8。這個(gè)坑對(duì)你的用戶來說可能沒有意義但只要你的工具要跨平臺(tái)分發(fā)就必須在一開始就統(tǒng)一編碼策略。4.3 遠(yuǎn)程終端里沒有剪貼板必須學(xué)會(huì)優(yōu)雅降級(jí)第三個(gè)坑是遠(yuǎn)程會(huì)話。我有相當(dāng)一部分時(shí)間是在連接服務(wù)器操作這時(shí)候如果執(zhí)行cua copypyperclip 通常會(huì)報(bào)錯(cuò)因?yàn)檫h(yuǎn)程 Linux 環(huán)境里沒有剪貼板協(xié)議也沒有安裝 xclip、xsel 之類的輔助工具。有些情況下即使做了 X11 轉(zhuǎn)發(fā)剪貼板也可能連不上總之遠(yuǎn)程和剪貼板之間經(jīng)常是徹底的失敗。我在實(shí)現(xiàn)里加了一個(gè)環(huán)境檢測(cè)當(dāng)檢測(cè)到當(dāng)前會(huì)話來自遠(yuǎn)程連接時(shí)copy命令自動(dòng)降級(jí)為直接打印內(nèi)容到終端并額外顯示一個(gè)提示告訴你這段應(yīng)該手動(dòng)選擇復(fù)制def cmd_copy(name: str): snippet find_and_load(name) if os.environ.get(SSH_CONNECTION): print(snippet[body]) print(# (remote session: clipboard unavailable, copy this manually)) return import pyperclip pyperclip.copy(snippet[body]) print(fcopied {name})用環(huán)境變量來做判斷算不上什么高深技巧但它很實(shí)用本地會(huì)話不會(huì)誤傷遠(yuǎn)程會(huì)話又能正常工作。如果你在 tmux、容器或遠(yuǎn)程開發(fā)環(huán)境里也用這個(gè)工具建議你也考慮類似的降級(jí)方案。工具本身的能力邊界不是死的在受限環(huán)境里能不能給出一個(gè)可用的替代方案往往決定了你是否愿意繼續(xù)使用它。4.4 多臺(tái)機(jī)器之間的同步文件化紅利與敏感內(nèi)容提醒第四個(gè)坑是多設(shè)備同步。因?yàn)?cua 的數(shù)據(jù)就是純文本文件我直接把它們交給一個(gè)私有 git 倉庫管理。在~/.cua目錄里初始化倉庫每次內(nèi)容變更后手動(dòng)提交一次。這樣我在辦公室電腦上新增的片段回到家里拉一下最近的變更就能看到遇到誤刪除還能從 git 歷史里恢復(fù)。如果你不想用 git用任何支持增量同步的私有工具數(shù)同步目錄也可以。但這里必須提醒一句cua 里保存的內(nèi)容很可能包含敏感信息比如內(nèi)網(wǎng)數(shù)據(jù)庫地址、帶訪問密鑰的配置、臨時(shí)生成的密碼。這些東西一旦被推到公共倉庫就是嚴(yán)重事故。我的做法是在~/.cua下放一個(gè).gitignore把secret-*這類特別命名的文件排除在外另外對(duì)少數(shù)真正敏感的片段用 gpg 單獨(dú)加密后再保存需要復(fù)制時(shí)先手動(dòng)解密。同步便利和安全永遠(yuǎn)是個(gè)權(quán)衡建議你先把“哪些片段可以同步”想清楚再?zèng)Q定是否開啟這項(xiàng)功能。5. 把 cua 變成肌肉記憶工作流整合與進(jìn)階玩法5.1 在編輯器里直接取片段cua 最舒服的上手方式是在編輯器里直接調(diào)用。比如在 vim 里我經(jīng)常用:r !cua grab db-url把某段配置直接讀進(jìn)當(dāng)前文件在 neovim 里還可以把這段調(diào)用再包一層快捷鍵。這樣我在寫代碼時(shí)不需要離開編輯器也不需要切到終端去復(fù)制粘貼已經(jīng)把 cua 當(dāng)成了輸入法之外的另一種“補(bǔ)全來源”。也許有人會(huì)問編輯器不是已經(jīng)有 snippets 插件了嗎確實(shí)有但那些插件通常綁定特定語言需要維護(hù)復(fù)雜的觸發(fā)詞配置。cua 的優(yōu)勢(shì)是完全通用只要內(nèi)容是我曾經(jīng)存過的東西不管它是 shell 命令、SQL 語句還是配置文件我都能用同一套記憶方式把它抓回來。它不是一個(gè)代碼補(bǔ)全插件而是一個(gè)樸素的文本倉庫正是這種樸素讓它能融入各種工具鏈。5.2 給 fuzzy finder 當(dāng)數(shù)據(jù)源如果你還嫌 cua 的命令行交互不夠直觀可以把它的列表輸出接給一個(gè) fuzzy finder 類的交互式選擇器比如 fzf。我常用的一行組合命令是這樣的cua list --with-description | fzf --preview cua grab {} --bind enter:become(cua copy {})這條命令會(huì)先展示所有片段名稱和描述預(yù)覽窗口里顯示選中片段的具體內(nèi)容按下回車就直接復(fù)制到剪貼板。原本需要先猜關(guān)鍵詞再跑 grab 的操作變成了上下移動(dòng)加回車整個(gè)過程基本不需要記憶任何片段名。這也讓我進(jìn)一步體會(huì)到命令行工具之間通過純文本協(xié)作是多么高效。cua 輸出的是人可讀的列表fuzzy finder 負(fù)責(zé)交互選擇兩者都沒有為此專門開發(fā)任何接口。5.3 我打算繼續(xù)加的三件小事用了一段時(shí)間之后我給自己列了一個(gè)很小的待辦清單都是能明確提升使用體驗(yàn)的改進(jìn)方向而不是功能堆疊。第一是模板變量。在片段里保留{{date}}、{{host}}這樣的占位符復(fù)制時(shí)用當(dāng)前日期或環(huán)境變量替換。這樣那些“帶日期的提交說明”“帶服務(wù)器名的部署命令”就不用每次手動(dòng)改一遍。第二是使用頻率統(tǒng)計(jì)。每次 copy 成功后在片段文件名旁邊加一個(gè)計(jì)數(shù)文件列表時(shí)按熱度排序這樣最常用的片段永遠(yuǎn)排在最前面。第三是 git 快照自動(dòng)提交。如果檢測(cè)到片段目錄已經(jīng)是一個(gè) git 倉庫就在每次增刪改之后自動(dòng)打一個(gè)提交免去手動(dòng)提交的負(fù)擔(dān)。這三件事我都刻意控制在很小的范圍內(nèi)。做 cua 這個(gè)工具最大的體會(huì)就是越是小工具越要克制把一條命令打磨到每天用上幾十次比做一個(gè)擁有一百個(gè)功能但沒人記得住的軟件有意義得多。如果你也想給自己做一把順手的小工具不要從完美設(shè)計(jì)開始從你每天最煩躁的那個(gè)操作開始。