最近在折腾本地知识库索引方案的时候,遇到了一个很常见的问题:每次新增或修改笔记之后,需要手动触发一次索引重建脚本,时间长了非常容易忘记。轮询方案(每隔几秒扫描一次目录)倒是能解决问题,但代价是 CPU 和 I/O 的持续消耗,在有几万个文件的知识库里显得格外浪费。这时候朋友推荐了 [[fswatch]],用了之后觉得这才是正确的打开方式。

fswatch 文件系统监控示意图

fswatch 是什么

fswatch 是一个开源的跨平台文件系统监控工具,由 Enrico M. Crisostomo 开发并维护,项目托管在 GitHub 上。它的核心思路很简单:订阅操作系统提供的文件系统事件通知接口,当指定目录或文件发生变化时,立即输出变化的路径,或者直接触发用户定义的命令。

和轮询方案最大的区别在于,fswatch 完全依赖内核级别的事件推送,自己不做任何定时扫描。在 macOS 上它使用 [[FSEvents]] API,这是苹果系统内建的机制,[[Spotlight]] 和 [[Time Machine]] 都依赖它工作。在 Linux 上则对应 [[inotify]] 或 [[kqueue]],在 BSD 系统上使用 kqueue。这意味着不管你的目录里有多少文件,监控本身对系统资源的占用几乎可以忽略不计,事件延迟也在毫秒级别。

安装

macOS 用户直接通过 [[Homebrew]] 安装,一行命令搞定:

brew install fswatch

Linux 上可以通过各发行版的包管理器安装,或者从源码编译。Ubuntu/Debian 用户可以:

sudo apt install fswatch

安装完成后运行 fswatch --version 确认版本即可。

基本用法

最基础的用法是直接监控一个目录,有变化时把变化的文件路径打印到标准输出:

fswatch /path/to/dir

每次有文件被创建、修改或删除,对应的路径就会出现在终端里。如果想把这个输出接入自动化脚本,最常见的模式是配合 xargs 使用:

fswatch -o /path/to/dir | xargs -I{} your-command

-o 参数让 fswatch 不输出具体路径,只输出一个数字(触发的事件总数),这样 xargs 每次只会执行一次命令,适合需要批量重建而不是逐文件处理的场景。

常用参数

fswatch 提供了丰富的过滤选项,让你精确控制哪些变化值得关注。

--include--exclude 可以用正则表达式过滤路径,两者可以组合使用:

# 只监控 .md 文件的变化,忽略其他所有文件
fswatch --include='\.md$' --exclude='.*' /path/to/dir

注意这里 --exclude='.*' 先排除所有,再用 --include 把需要的加回来,顺序很重要,fswatch 按规则声明的先后顺序依次匹配。

-r 参数开启递归监控,会深入子目录:

fswatch -r /path/to/dir

--event 可以限定只监听特定类型的事件,比如只关心文件内容修改而不关心属性变化:

fswatch --event Updated --event Created /path/to/dir

实际应用场景

我自己用 fswatch 最多的场景是本地知识库的自动化索引。我的 [[Obsidian]] 笔记库有几万个文件,每次手动重建 SQLite 全文索引很麻烦。加了 fswatch 之后,只要有 .md 文件变动,索引就自动增量更新:

fswatch -o --include='\.md$' --exclude='.*' ~/Sync/wiki \
  | xargs -I{} python3 ~/Sync/wiki/scripts/build_index.py

另一个常用场景是前端开发时自动触发构建。虽然现在很多构建工具([[Vite]]、[[webpack]])自带 watch 模式,但遇到需要自己写脚本处理的情况,fswatch 比自己实现一个轮询循环要靠谱得多:

fswatch -o src/ | xargs -I{} npm run build

还有一个不太常见但很实用的场景:监控日志文件,某些关键字出现时发送桌面通知:

fswatch /var/log/app.log | while read path; do
  if grep -q "ERROR" "$path"; then
    osascript -e 'display notification "检测到错误日志" with title "监控告警"'
  fi
done

使用时需要注意的地方

fswatch 默认是异步输出,如果短时间内有大量文件变动(比如 git checkout 切换分支),会产生大量事件并发触发,导致命令被执行很多次。这种情况下可以配合 --batch-marker 或者在脚本里加一个简单的防抖逻辑,避免短时间内重复执行。

另外,在 macOS 上 FSEvents 的事件粒度是目录级别的,也就是说即使只改了一个文件,fswatch 输出的路径也可能是该文件所在的目录,而不是具体的文件名。这个行为在某些需要精确文件路径的场景下需要额外处理。如果需要更精确的粒度,可以尝试 --monitor=poll_monitor 切换到轮询模式,但这样就失去了零开销的优势。

还有一点是 fswatch 进程本身不会自动重启。如果监控进程意外退出,需要有额外的进程守护机制(比如 [[launchd]] 或 [[supervisor]])来保证它一直在运行。

最后

fswatch 让我彻底告别了”改完文件别忘了手动跑脚本”的习惯,把那些本应自动化的步骤真正变成了自动化。它的设计哲学和 Unix 的管道思想高度契合——做好一件事,然后通过标准输出和其他工具组合。对于 macOS 用户来说,基于 FSEvents 的实现让监控本身几乎没有额外开销,不管库里有多少文件都能保持响应灵敏。如果你有任何”文件变化时自动做 X”的需求,fswatch 几乎是最低成本、最直接的解决方案。