☰
caveman:用纯文本和Git打造永不丢失的本地收藏夹
2026/10/8 21:12:53 网站建设 项目流程

收进收藏夹的东西,基本等于再也找不到。“caveman”这个热词最近总在我首页刷到,我第一反应不是原始人钻木取火,而是一个更具体的画面:信息碎片像猎物一样被拖回本地洞穴,整整齐齐码好,大冷天窝在洞里慢慢翻。于是我写了caveman——一个把网页链接、临时备忘、代码片段用纯文本收进本地的命令行小工具。

这套方案没有数据库,没有 Web UI,没有任何需要保持登录的服务,核心就是 Bash 函数加 Git。它解决的是我这几年最头疼的“收藏病”:链接存了三千多条,真到用的时候一个都找不到。如果你也在 Notion、浏览器书签、微信收藏之间反复横跳,又不想继续给各个云端大佬当数字佃农,这篇文章值得你花十分钟看完。

1. 从“链接吃灰”到“caveman”:我为什么写这个原生方案

1.1 数字囤积患者的一天

先说一个场景,你看看眼不眼熟。早上刷到一篇讲古法铸铁锅开锅的文章,觉得以后会用上,顺手放进了浏览器书签;中午同事发来一段 Python 脚本,微信收藏里存一份;下午查技术方案看到一篇资料,存到 Notion 的某个看板;晚上读到一句有启发的句子,拍下来扔进了相册“灵感”相册。

两周后你真需要铸铁锅开锅步骤时,你的搜索路径是:打开 Chrome 书签,翻,没有;打开微信收藏,搜“铁锅”,出来一堆表情包;打开 Notion,发现你当时压根没建数据库,只是把链接丢在了一个叫“未整理”的页面里;最后你在相册里划了半天,终于找到一个模糊的截图,但后面那两条关键细节被截掉了。

我那天晚上算了笔账:我在这几个平台之间来回切换、重复搜索、确认“是不是旧版本”的时间,加起来比原始人钻木取火还原始。问题根本不在于“存没存”,而在于“存完能不能在一个地方找回来”。我需要的不是一个功能更全的笔记软件,而是一个足够笨、足够稳、永远不背叛我的存放点。

1.2 一句话定位:caveman 是什么、不是什么

caveman是我写的一个约 200 行的 Bash 工具集,现在安静地躺在~/.caveman里。它的工作方式极其直白:你给它一条信息,它按当前年月生成一个带时间戳的纯文本文件,塞进本地目录,然后用 Git 提交一次。就这么多。

它保存的东西可以是一条 URL、一个随手记的想法、一段复制的代码,也可以是一整段笔记正文。所有数据都是.txt文件,UTF-8 编码,文件命名类似2024-04-07_153022.txt,打开就能读。搜索方式也原始:grep或者rg,找到文件后用sed看内容。

同时,caveman不是什么。它不抓取网页全文(想要的话可以自己接curl),不做 AI 自动分类,不做标签图谱,不出跨平台客户端。这些需求本身没有错,但它们不是“存下来、找得到”这件事的必需品。所有额外功能都会变成运行依赖,而运行依赖会变成你某天不想再打开它的理由。

1.3 四条设计原则背后的人话解释

第一条,数据必须在我手里。云收藏夹今天还活得好好的,明天可能改版把“稍后读”藏进二级菜单,后天可能直接停止服务。但一个.txt文件,放在我的磁盘上,二十年后打开也还是那几行字。我把这叫做“洞窟所有权”。

第二条,纯文本优先。Markdown 是底线,我连 YAML front matter 都嫌重,只保留# 标题、url:、tags:、created:四行元信息,后面跟一条---分隔线正文。文本每行都能diff,能进 Git,能用任何编辑器改,不受某个 App 的私有格式绑架。

第三条,最小依赖。默认只需要 Bash、GNU coreutils、Git。UI 交互里我用到了 fzf,但它是可选依赖,没有 fzf 照样能搜索。这样做的直接好处是我可以在任何一台刚装好系统的 Linux、macOS 或 Termux 手机上,五分钟内把整套工作流拉起来。

第四条,从看到到入库,三秒内完成。所有命令都围绕“快”设计:caveman add只需要一个标题参数,剩下都能省略;搜索命令caveman search加上关键词就出结果。任何需要先打开 App、再点三个按钮、再等加载的操作,都会让人最终放弃记录。

2. 先照抄:三分钟搭好你自己的洞穴仓库

2.1 目录结构与初始化脚本

我不太喜欢装完一堆依赖之后再初始化。caveman的仓库结构非常简单,你可以直接在 shell 里敲两行命令建出来:

mkdir -p ~/.caveman/entries ~/.caveman/snapshots cd ~/.caveman git init

目录长这样:

~/.caveman/ ├── entries/ │ └── 2024/ │ └── 04/ │ ├── 2024-04-07_153022.txt │ └── 2024-04-07_160843.txt ├── snapshots/ # 以后想存网页快照时再启用 └── .git/

为什么用2024/04这样的年月分目录?因为文件一旦多起来,纯grep搜全库仍然很快,但备份、按月份打包、按时间归档这些操作都更顺手。比如哪天你想清点三月份收了多少条,直接find entries/2024/03 -type f | wc -l就完了。

接下来把两个函数挂进 shell。我习惯在~/.bashrc里写:

export CAVE_DIR="$HOME/.caveman" source "$HOME/.caveman/cave.sh"

cave.sh就是核心脚本文件,稍后我会带你过一遍关键部分。如果你用的是 zsh,把.bashrc换成.zshrc,函数照样兼容。

2.2 日常使用:存网页、存备忘、找东西

初始化完成后,你的三个高频命令就是 add、search、backup。存一条网页链接:

caveman add "如何给铸铁锅开锅" "https://example.com/cast-iron?from=vlog&lang=zh" "生活,做饭"

存一条无 URL 的纯备忘:

caveman add "周六去五金市场买铁钉" "" "备忘"

查找,直接给关键词:

caveman search "铁锅"

看一眼今天之前的若干条:

caveman list --months 3

每次 add 之后,脚本会把文件写进对应目录,然后立刻提交一次 Git:

saved: ~/.caveman/entries/2024/04/2024-04-07_153022.txt [main abc1234] add: 如何给铸铁锅开锅 1 file changed, 5 insertions(+)

你会发现每个条目就是一次 commit,这意味着你不仅能搜内容,还能通过 Git 查看某一天到底存了什么、什么时候删过哪些记录。这在传统笔记软件里想都不敢想。

2.3 给命令加 PATH 与一个偷懒的别名

把脚本放好后,我建议在.bashrc里加一行别名,拯救那些脑子里只有“收藏”两个字的人:

alias cave='caveman'

想偷懒的时候直接敲cave add。至于 Tab 补全,最省事的办法是先跑通主流程,再考虑锦上添花。一个极简补全长这样:

_caveman_completion() { local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=( $(compgen -W "add search list backup" -- "$cur") ) } complete -F _caveman_completion caveman

这行代码会把 add / search / list / backup 四个子命令补出来,值不了多少钱,但能让手速快上一截。注意,真没必要一开始就折腾补全,参数解析都还没稳定,补全反而容易带偏。

3. 核心逻辑拆解:追加写、grep 搜索与 Git 冷备份

3.1 追加写:为什么它比数据库更适合个人流水

先上核心的cave_add函数,这是我后来反复打磨的版本:

cave_add() { local title="$1" url="${2:-}" tags="${3:-}" # 没有标题时直接把 URL 当标题,避免写下一堆空文件 if [ -z "$title" ] && [ -n "$url" ]; then title="$url" fi [ -z "$title" ] && { echo "用法: caveman add <标题> [URL] [标签]"; return 1; } local stamp y m dir file stamp="$(date +%Y%m%d_%H%M%S)" y="$(date +%Y)" m="$(date +%m)" dir="$CAVE_DIR/entries/$y/$m" mkdir -p "$dir" file="$dir/${stamp}.txt" { printf '# %s\n' "$title" printf 'url: %s\n' "$url" printf 'tags: %s\n' "$tags" printf 'created: %s\n' "$(date -Iseconds)" printf -- '---\n\n' } > "$file" echo "saved: $file" git -C "$CAVE_DIR" add "$file" >/dev/null git -C "$CAVE_DIR" commit -m "add: $title" >/dev/null 2>&1 || true }

这个方法的核心是“追加写”。每个条目独立一个文件,新文件只会被创建,不会回头去修改老文件。相比一个巨大的 JSON 或 SQLite 数据库,追加写的优势非常直观:

写入永远是顺序写,系统层面开销极低;单个文件坏了只丢一条记录,不会整个库损坏;你可以用文件管理器直接浏览,不需要任何工具就能“破译”数据。

有人会问,文件多了怎么办?我现在的使用强度下,一年撑死三千个文件,一个目录里几百个 txt,Linux 文件系统处理这点数量跟玩一样。真到了上万个文件那天,加一层按 tags 分目录就行。个人数据流的写模式是“只插不删”,这是文件系统的天然主场,跑到数据库里反而是找罪受。

3.2 搜索演进:从 grep 到 fzf,再到 ripgrep

搜索是这套工具的灵魂,也是我花时间最多、迭代感最强的地方。第一版就三行:

cave_search() { local q="$*" [ -z "$q" ] && { cave_list; return; } grep -rli -- "$q" "$CAVE_DIR/entries" 2>/dev/null | head -50 }

grep -rli解释给新手听:-r递归目录,-l只输出包含匹配内容的文件名,-i忽略大小写。这样搜索不会把整个文件内容打出来刷屏,而是先给出一串文件路径,你挑一个再看。

但纯路径列表不够友好,尤其文件多达几十个时,你不知道每个文件里到底写了什么。于是我把 fzf 加进来,让结果变成可交互的选择器:

cave_search() { local choice choice="$( grep -rli -- "$*" "$CAVE_DIR/entries" 2>/dev/null | fzf --preview 'sed -n "1,12p" {}' --preview-window=up:60% )" [ -n "$choice" ] && sed -n '1,40p' "$choice" }

这里的--preview 'sed -n "1,12p" {}'意思是:当你上下移动光标时,fzf 会调用sed打印对应文件的头 12 行,你还没回车就能瞥见标题、URL 和标签,判断是不是自己要找的。选中的文件再用sed -n '1,40p'把正文前 40 行打出来。

如果哪天目录里文件超过两万,可以把 grep 那行换成:

rg -l --no-messages -- "$q" "$CAVE_DIR/entries" | ...

ripgrep 的搜索速度是 grep 的几倍到十几倍,输出更干净,对隐藏文件、二进制文件有一套默认的理智行为。但就我的经验,现阶段 grep 足够,ripgrep 不是必需品。一个工具如果连基本功能都还没跑顺,先别急着上性能方案。

3.3 Git 才是真正的数据库

很多笔记工具把“同步”放在云端,而我把版本控制放在了本地。每个条目一次git commit,意味着整个仓库就是一条不断延伸的时间线。某天我发了条备忘,后来把它删了,但我能通过git log --diff-filter=D --summary看到删除记录,随时找回来。

备份函数也很简单:

cave_backup() { git -C "$CAVE_DIR" add -A git -C "$CAVE_DIR" commit -m "chore: backup $(date -Iseconds)" || true git -C "$CAVE_DIR" push origin HEAD 2>/dev/null || echo "没有配置远程,跳过推送" }

如果~/.caveman配置了 Git 远程,比如你自己搭的 Gitea、GitLab 或者一个私有仓库,每次 backup 都会把最新数据推到远端。没有远程也没关系,让 commit 留在本地,每个月手动用git bundle打包一次丢进移动硬盘,这叫冷备份。

为什么不用 rsync 定期覆盖备份?因为 rsync 只处理“当前状态”,而 Git 保存的是“每次演进的历史”。个人数据最重要的不是最终一致,而是你知道上个月那条记录还在、中间改了什么、谁改的。纯文本让 Git 的 diff 变得人眼可读,这是一开始坚持纯文本最大的红利。

4. 踩坑实录:特殊字符、编码问题与同步冲突的完整排查链路

4.1 第一现场:含 & 和中文的 URL 被切断了

任何脚本用到真实世界 URL 都会撞见这幕。某天我用 caveman 收藏一条搜索结果页:

caveman add "测试搜索" "https://example.com/query?q=铁锅&page=2" "测试"

入库之后打开文件,发现url:那行变成了:

url: https://example.com/query?q=铁锅

&page=2从世界蒸发了。第一反应是难道 shell 引号没加对?可我明明已经把 URL 放在双引号里了。于是我直接在终端里手动跑了一遍,还是一样。

这里就是第一个坑:传给函数内部的$2时,双引号只保护了“从命令行传到函数参数”这一段,但函数内部如果用了类似echo "$url"这样的语句,在旧版 Bash 或某些环境下仍然可能因为内部的关键词展开丢失内容。我的早期版本恰恰用了echo $url而不是echo "$url",变量没加引号,单词分割就把&后面的内容当新参数处理了。

排查链路往往是这样的:先怀疑调用姿势 → 逐层打印参数 → 发现函数里echo $url丢内容 → 把变量引号补上 → 问题消失。这事教训只有一个:外部输入永远放在双引号里,输出外部内容永远用printf而不是echo。

4.2 根因分析:echo 与引号的边界、还有那个看不见的换行

上面案例修起来容易,根治却不简单。要理解为什么printf比echo稳,得说清楚一个核心差异:echo对不同 shell 的转义和-n之类的参数处理不一致,而printf的格式串是确定的。

写文件那一段,我把所有输出全部改成:

printf '# %s\n' "$title" printf 'url: %s\n' "$url" printf 'tags: %s\n' "$tags"

凡是用户提供的数据,作为参数传给%s,由printf控制格式。这样即使 URL 里带%、&、空格、单引号,都能一字不差落到文件里。

第二个隐蔽问题是“不可见字符”。有些从网页复制过来的标题,末尾带着一个零宽空格或者 Windows 换行符\r,printf 照单全收,之后 grep 搜“标题”就总差最后半个字。一开始我以为自己把它写残了,后来用xxd看了二进制才发现。解决办法是在 add 时做一步清洗:

title="$(printf '%s' "$title" | tr -d '\r')"

\r删掉,语言层面的杂音也顺便去了。这不算聪明,算吃过亏长记性。

4.3 冲突风波:多设备同时入库时,Git 合并把文件弄乱了

caveman 的进阶玩法是多设备同步:公司电脑、家里电脑、手机 Termux,都拉到同一个仓库。某个周一早上,我在手机上存了一条通勤地铁路线备忘,家里电脑昨晚也存了一条,两边都没 pull 对方的最新数据。第二天在公司 pull 时,Git 提示生产了一个 merge commit,我一看git log --graph,叉子形状的线出来了。

好在因为我坚持一个条目一个文件、文件名带秒级时间戳,两台设备同时新增文件时几乎不会撞名,合并出来两个文件互相独立,不看git log根本感觉不到。真正的麻烦来自“同一个文件被两边改”的场景,比如我用手机补写了某条备忘的正文,电脑上又给同一条补了张图片路径,pull 时冲突就来了。

这个坑的根因是:我在设计之初默认条目不可变,但实际使用中经常想补写。纯文本补写的代价比笔记软件高,因为文件系统没有“就地更新”这个概念。我的对策是两条:

第一,让补写变成一个独立动作,生成一个“修订条目”,而不是直接改动原文件。比如原文件叫20240407_153022.txt,我补写就新建20240407_153023.txt,内容里用ref:指向原条目。两边的修改永远不碰同一个文件。

第二,Git 配置里设置:

git -C "$CAVE_DIR" config pull.rebase true

把 pull 改成 rebase,提交历史保持线性,避免交叉合并记录越来越多。多设备同步到现在大半年,没有再出现需要手工解冲突的情况。

4.4 修复与验证:让命令自己长记性

修完这些坑之后,我写了一个caveman doctor命令,专门做环境检查,免得每次换了新设备都要靠血液循环找问题:

cave_doctor() { echo "== 目录 ==" [ -d "$CAVE_DIR/entries" ] && echo "entries 存在" || echo "entries 缺失" command -v git >/dev/null || echo "缺少 git" command -v fzf >/dev/null && echo "fzf 可用" || echo "fzf 未安装(可选)" echo "== 语言环境 ==" printf 'LANG=%s\n' "${LANG:-未设置}" echo "== 最近 5 次提交 ==" git -C "$CAVE_DIR" log --oneline -5 2>/dev/null || echo "仓库未初始化" }

每次换电脑、换手机、重装系统,先跑一句caveman doctor,三秒钟知道自己缺什么。之后再跑一次当年出问题的命令,用cat看那个 txt 文件,确认 URL 完整落地,确认中文无乱码。给所有外部输入加引号、统一 UTF-8、优先printf,这三个习惯救了整个项目。

5. 给洞穴装上窗户:浏览器、手机与收尾的小扩展

5.1 从浏览器一键投喂

命令行再好,手动复制粘贴标题和 URL 还是有点麻烦。我后来给caveman加了一个极简的本地 HTTP 接口,让浏览器能一键投喂。

加一个serve子命令,监听127.0.0.1的 8765 端口,收到请求后把参数转给cave_add。核心代码二十行左右,不必上框架:

#!/usr/bin/env python3 import http.server, subprocess, urllib.parse class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) title = q.get('title', [''])[0] url = q.get('url', [''])[0] tags = q.get('tags', ['caveman'])[0] if title or url: subprocess.run(['caveman', 'add', title, url, tags], capture_output=True, text=True) self.send_response(200) self.end_headers() self.wfile.write(b'ok') if __name__ == '__main__': http.server.HTTPServer(('127.0.0.1', 8765), Handler).serve_forever()

然后在浏览器里存一个书签,地址写:

javascript:location.href='http://127.0.0.1:8765/?title='+encodeURIComponent(document.title)+'&url='+encodeURIComponent(location.href)

看到想存的内容,点一下这个书签,页面跳一下显示ok,信息已经进了本地洞穴。因为服务只监听环回地址,外界根本访问不到,安全性足够。这一节的意义在于:工具要真的融入日常,不能总让人先切到终端敲命令。

5.2 在 Termux 上低成本接入手机

手机端我用了 Termux,安装完成之后依次执行:

pkg install git bash coreutils fzf mkdir -p ~/.caveman git clone 你的远程仓库地址 ~/.caveman

然后把cave.sh里CAVE_DIR指到克隆下来的目录。Android 上要注意两点:一是确认仓库里的文本都是 UTF-8,git config --global core.autocrlf false关掉换行符转换;二是系统 locale 可能不是 UTF-8,建议执行:

export LANG=en_US.UTF-8

这样中文搜索才能稳定命中。

在手机端我几乎只做 add 和 search。走在路上看到一块广告牌上的灵感,复制链接,Termux 里cave add一条,晚上回家电脑上 search 一下就能接着展开。这个体验比在手机上打开十个收藏夹 App 来得踏实。

5.3 从流水账到个人知识库:下一步

当仓库里攒了上千条记录,纯文本底座的第二层价值就显现了:可以毫无心理负担地写统计、写转换脚本。比如我现在每个月末会跑一个粗糙的“本月捕获统计”:

cave_stats() { local y m y="$(date +%Y)" m="$(date +%m)" find "$CAVE_DIR/entries/$y/$m" -type f -name '*.txt' | wc -l }

它能告诉我这个月到底捕获了多少条信息,存了多少“可能有用”的东西。如果数量暴涨,说明我又开始水收藏了;如果暴跌,说明最近在认真干活。

再往下走,可以用一个for循环抽全部文件里的tags:行,用awk按逗号拆开排序,生成一份简单的标签频率表。真到了想找某个领域的所有积累时,grep -rl "tags:.*做饭" ~/.caveman/entries一行就能捞干净。

至于要不要生成 RSS、要不要接大模型做自动摘要,我目前的答案都是:等需求真的出现再说。工具最好的状态是长在你手上,而不是替你思考。现在每次打开~/.caveman/entries,看到那些按年月排好的 txt 文件,我就想起洞穴壁画上整齐的刻痕——数据回到自己手里的感觉,比什么热词都踏实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询