- 开发工具
- CLI
【免费下载链接】watchexec
Executes commands in response to file modifications
本篇文章围绕 Watchexec 生态中承担默认过滤职责的watchexec-filterer-globsetcrate 展开,结合其 CHANGELOG.md、README.md 与 源码实现,完整讲解它的定位、构造 API、事件级与目录级双重过滤语义、ignore 文件集成、CLI 参数映射以及从 v1.0.0 到 v8.0.4 的演进脉络。读完本文,你将理解 Watchexec 的--filter、--ignore、--exts等选项在底层究竟如何被解释执行,也能掌握自顶向下剪枝语义这一关键设计对文件监听性能的影响。
一、定位:Watchexec 的默认过滤实现
watchexec-filterer-globset是 Watchexec 工作区中的一个独立 crate,其 README 明确自述为:
The default filterer implementation for Watchexec.
也就是说,Watchexec CLI 当前使用的默认过滤器正是它。它是一套"仅基于路径"(path-only)的过滤器,在 crates/filterer/globset/src/lib.rs 的 crate 文档中这样描述:
本过滤器模拟
watchexecv1 过滤器的行为,但由于内部实现不同,并不与其完全一致。它目前被用作 Watchexec CLI 的默认过滤器。
在 Watchexec 架构中,"过滤"由库级 trait 定义。crates/lib/src/filter.rs 中的Filterertrait 只要求实现者提供两个同步方法:
check_dir(&self, path) -> Result<bool, RuntimeError>:在协调文件系统事件源时被调用,用于决定一个目录是否应被纳入监听。返回false会把该目录连同其后代整体排除出事件源;文档明确要求"仅当该目录下所有事件都可以安全忽略时才拒绝它"。check_event(&self, event, priority) -> Result<bool, RuntimeError>:对(几乎)每一个事件调用,返回false表示丢弃该事件。
两个方法都被要求"同步、快速、不得阻塞线程",任何昂贵的工作都应该在构造阶段完成。GlobsetFilterer正是遵循这一契约的实现:它在new()中完成所有编译与快照,运行时只做匹配。
二、核心构造:GlobsetFilterer::new的六类输入
GlobsetFilterer的结构体定义(crates/filterer/globset/src/lib.rs)包含六个成员,与构造函数一一对应:
| 构造参数 | 成员类型 | 作用 |
|---|---|---|
origin | PathBuf | 项目原点(project origin),所有 glob 的相对基准 |
filters | Gitignore | 正向过滤 glob 集合:只有匹配的路径才能通过 |
ignores | Gitignore | 反向忽略 glob 集合:匹配的路径将被拒绝 |
whitelist | HashSet<PathBuf> | 精确路径白名单:即使被过滤也强制放行 |
ignore_files | IgnoreFilterer | 来自 ignore 文件(.gitignore等)的过滤规则 |
extensions | Vec<OsString> | 扩展名白名单:只放行这些扩展名的文件 |
构造函数的完整签名(crates/filterer/globset/src/lib.rs):
pub async fn new( origin: impl AsRef<Path>, filters: impl IntoIterator<Item = (String, Option<PathBuf>)>, ignores: impl IntoIterator<Item = (String, Option<PathBuf>)>, whitelist: impl IntoIterator<Item = PathBuf>, ignore_files: impl IntoIterator<Item = IgnoreFile>, extensions: impl IntoIterator<Item = OsString>, ) -> Result<Self, Error>几个关键的构造语义:
- glob 的作用域:
filters与ignores每一项都是(模式字符串, Option<路径>)元组。Some(path)表示该模式只在指定文件夹内生效(例如某个.gitignore所在的目录),None表示全局生效。构造时二者分别喂给GitignoreBuilder::add_line编译成两棵独立的Gitignore结构。 - 原点规范化:
origin先经dunce::canonicalize得到真实路径,再经simplify_path(即dunce::simplified(path).normalize())消解..与路径别名(crates/filterer/globset/src/lib.rs)。白名单路径同样在构造期做simplify_path,保证与事件路径比较时二者处于同一形态。 - 空集合语义:若
filters为空,则只使用ignores;若两者皆为空,过滤器对所有路径放行(对应测试 empty_filter_passes_everything)。 - 错误处理:路径规范化失败产生
Error::Canonicalize,glob 编译失败产生Error::Glob,二者都来自ignore_filescrate 的错误体系。 - full_debug feature:
Gitignore类型的Debug实现会输出大量内部信息,默认实现将其遮蔽为"ignore::gitignore::Gitignore{...}";启用 Cargo.toml 中定义的full_debugfeature 后恢复完整Debug(这正是 CHANGELOG v4.0.1 的改动内容)。
三、事件级过滤(check_event)的判定顺序
check_event是过滤器的主战场。它的执行顺序(crates/filterer/globset/src/lib.rs)如下:
- 路径规范化:克隆事件,把所有
Tag::Path中的路径做simplify_path处理,后续所有比较都基于规范化后的路径。 - 白名单短路:若白名单非空,且事件携带的任一路径命中白名单,直接放行(
return Ok(true)),不再执行任何后续检查。 - ignore 文件检查:交给内部
IgnoreFilterer.check_event;被 ignore 文件命中则拒绝。该调用被expect("IgnoreFilterer never errors")包裹,因为IgnoreFilterer的实现保证不返回错误(crates/filterer/ignore/src/lib.rs)。 - 非路径事件放行:如果事件不携带任何路径 tag(例如键盘、信号、内部事件),直接通过。
- 逐路径判定:对事件中的每个路径执行
paths.any(...)——只要有一个路径通过,事件即通过。每个路径依次经过:- globset ignore 检查:
ignored_by_globs(见下节),被忽略则此路径失败; - 正向过滤:若
filters非空,用filters.matched(path, is_dir)匹配,命中is_ignore(即被该 glob 包含)则放行; - 1.x 兼容重映射:在 Unix 上保留了一个 Watchexec 1.x 的兼容行为(代码注释标记为"1.x bug, TODO remove at 2.0"):当路径位于 origin 之下时,会构造一个
origin + 双分隔符 + 相对路径的"重映射路径"再匹配一次,模拟 v1 的怪异行为(对应测试 extensions_and_filters_glob 中注释"Watchexec 1.x buggy behaviour"的部分); - 扩展名过滤:若
extensions非空,目录直接失败;有扩展名则看是否命中集合中的任意一项;无扩展名的文件失败(extensions_fail_extensionless); - 默认放行:若以上过滤条件一个都没启用(
filtered == false),路径通过。
- globset ignore 检查:
值得注意的优先级结论(对应测试 ignores_take_precedence):忽略规则优先于正向过滤——即使某个路径同时命中filters与ignores,结果仍是拒绝。而--exts扩展名过滤则是"与"关系之外的追加条件:既有 glob 过滤又有扩展名过滤时,两者必须其一命中才通过。
四、目录级过滤(check_dir):v8.0.2 引入的源码目录剪枝
check_dir是 v8.0.2 版本的核心新特性(CHANGELOG 原文):
Add source-directory filtering from ignore files and ignore globs, with top-down ancestor semantics suitable for pruning recursive walks. Keep positive filters, extension filters, and exact-path whitelists event-only so they do not prune possible matching descendants.
它的实现(crates/filterer/globset/src/lib.rs)非常简洁,只做两件事:
- 先交给
ignore_files.check_dir(path); - 再调用
ignored_by_globs(path, true)(以目录身份匹配 ignore globs)。
ignored_by_globs(crates/filterer/globset/src/lib.rs)实现的是自顶向下祖先语义:
- 先取路径相对 origin 的祖先链(
path.ancestors(),跳过自身,截断到 origin 边界),再从靠近 origin 的一侧(顶层)向叶子方向逐个用self.ignores.matched(ancestor, true)匹配; - 一旦某个祖先被 ignore glob 命中,立即判定整棵子树被忽略——后代的否定(negation,
!前缀)无法重新打开已经被剪枝的祖先; - 最后才匹配路径自身。
这种语义与文件系统遍历(walk)的方向一致,因此天然适合作为递归遍历的剪枝判断:遍历器在进入目录前先询问check_dir,被拒绝的目录连同其后代直接跳过,不必逐一检查。对应的测试包括:
- source_checks_manual_ignore_boundary_parent_and_negation:
**/prunes使prunes与prunes/nested目录级失败; - descendant_negation_does_not_reopen_ignored_parent:
parent/+!parent/child时,parent/child目录仍失败——被忽略的父目录不可被后代否定重开; - explicitly_unignored_parent_allows_descendant_negation:只有先对祖先本身显式否定(
!parent/),其下的否定才重新生效。
同时,v8.0.2 还明确了分层原则:正向过滤(filters)、扩展名过滤(extensions)、精确路径白名单(whitelist)只作用于事件级,不参与目录级判定。原因正如 CHANGELOG 所说:若用它们去剪枝目录,可能误伤"目录内含匹配后代"的情况。测试 source_checks_do_not_use_positive_filters_or_extensions 验证了:即使filters = ["**/*.rs"]、extensions = ["rs"],目录build、src在check_dir层面仍然放行;而 exact_whitelist_is_event_only_for_source_checks 验证了白名单"事件级放行、目录级仍拒绝"的行为。
此外,ignored_by_globs对边界情况有明确约定:路径在 origin 之外时,祖先列表为空,只精确匹配路径自身——因为过滤器不知道外部路径的遍历根,不能把项目相对 glob 应用到任意文件系统祖先上。测试 out_of_origin_paths_do_not_match_external_ancestors 与 relative_origin_stops_ancestor_matching_at_project_boundary 分别覆盖了外部路径与相对 origin 的场景。
五、ignore 文件集成:构造期快照,不监视改动
GlobsetFilterer的第五个参数是ignore_files: impl IntoIterator<Item = IgnoreFile>。IgnoreFile(crates/ignore-files/src/lib.rs)是一个携带三条元数据的结构:
path:ignore 文件路径;applies_in:该文件生效的子树,None表示全局 ignore;applies_to:该文件所属的项目类型(由project_originscrate 提供的ProjectType),例如仅当检测到 Git 仓库时才加载.gitignore。
构造时,这些文件被一次性编译进IgnoreFilter(来自ignore-filescrate),再包进 IgnoreFilterer(一个极薄的新类型包装,同时实现Filterer)。IgnoreFilterer的事件检查对每个路径调用match_path_or_ancestors——这正是与 globset 一致的自顶向下祖先语义(crates/ignore-files/src/filter.rs)。
一个重要行为在文档与 doc 注释中被反复强调:ignore 文件只在构造期间读取,不会被监视后续编辑;要加载新增或修改的 ignore 文件,必须重建过滤器(crates/filterer/globset/src/lib.rs)。这符合Filterertrait 的"昂贵工作放到构造阶段"契约。
六、在 Watchexec CLI 中如何组装
CLI 侧的组装代码在 crates/cli/src/filterer.rs:WatchexecFilterer包裹一个GlobsetFilterer,并在其外再叠加--fs-events文件系统事件类型过滤与--filter-progjaq 程序过滤。六个构造参数分别来自命令行:
GlobsetFilterer::new参数 | 对应 CLI 选项 | 说明 |
|---|---|---|
origin | --project-origin(或自动发现) | 项目原点;未指定时由 dirs::project_origin 按标记文件向上探测 |
filters | --filter/-f、--filter-file | 正向 glob,作用于工作目录(workdir) |
ignores | --ignore/-i、--ignore-file及内置默认 ignore | 反向 glob |
whitelist | --watch/-w、--watch-non-recursive等被监听路径 | 被显式监听的确切路径自动进入白名单,即使被 ignore 规则覆盖也会收到事件 |
ignore_files | --no-global-ignore、--no-vcs-ignore、--no-project-ignore、--no-discover-ignore等开关控制的自动发现 | 见下 |
extensions | --exts/-e | 支持.js或js两种写法,--exts解析时自动剥离前导点(crates/cli/src/filterer.rs) |
内置的默认 ignore 集合(除非--no-default-ignore)包括:.DS_Store、watchexec.*.log、*.py[co]、#*#、.#*、.*.kate-swp、.*.sw?、.*.sw?x,以及VCS_DIR_NAMES中的版本控制目录(.git、.hg、.svn、.bzr、_darcs、.fossil-settings、.pijul),每个目录同时匹配**/<目录>与**/<目录>/**两条 glob(crates/cli/src/filterer.rs)。
ignore 文件的自动发现则由ignore-filescrate 完成,覆盖 Git(.gitignore、.git/info/exclude、core.excludesFile)、Mercurial、Bazaar、Darcs、Fossil 以及 Watchexec 自身的.ignore,全局与项目级文件各有明确的查找顺序(crates/cli/src/args/filtering.rs)。--ignore-nothing是一个总开关,等价于同时禁用自动发现与默认 ignore(显式传入的--ignore仍生效)。
七、版本演进:从独立 crate 到默认过滤器(CHANGELOG 全记录)
CHANGELOG.md 记录了该 crate 的完整演进史,摘要如下:
- v1.0.0(2022-06-23):作为独立 crate 首次发布。
- v1.0.1(2022-09-07):依赖更新,miette 升至 5.3.0。
- v1.1.0(2023-01-09):MSRV(最低支持 Rust 版本)提升到 1.61.0。
- v1.2.0(2023-03-18):放弃 MSRV 策略(PR #510)。
rust-version字段保留,仅用于指示 crate 自身代码所需的最低 Rust 版本估计值,但依赖可能早已前进;此后只假定并测试最新稳定版。 - v2.0.1(2023-12-09):改为直接依赖
watchexec-events,不再经过watchexec的 re-export。 - v3.0.0(2024-01-01):引入对
watchexec-filterer-ignore与ignore-files的依赖——ignore 文件支持由此接入。 - v4.0.0(2024-04-20):升级到 watchexec 4。
- v4.0.1(2024-04-28):默认隐藏
ignorecrate 的Debug输出噪音,可用full_debugfeature 恢复。 - v5.0.0(2024-10-13):新增 whitelist 参数——精确路径白名单能力由此引入。
- v6.0.0(2024-10-14):升级到 watchexec 5。
- v7.0.0(2025-02-09):无条目说明(依赖或内部调整)。
- v8.0.0(2025-05-15):无条目说明。
- v8.0.1(2026-08-22):无条目说明。
- v8.0.2(2026-08-24):引入目录级过滤——从 ignore 文件与 ignore globs 增加 source-directory 过滤,采用适用于递归遍历剪枝的自顶向下祖先语义;同时明确正向过滤、扩展名过滤与精确路径白名单保持事件级,不做目录剪枝(详见本文第四、五节)。
- v8.0.3(2026-09-03)、v8.0.4(2026-09-15):无条目说明,当前最新版本,与 Cargo.toml 中
version = "8.0.4"一致。
从时间线可以看出,该 crate 与watchexec库版本号同步跳跃(4→5→8),而rust-version = "1.61.0"至今保留在 Cargo.toml 中,作为对 1.2.0 决策的延续。CHANGELOG 中头部还有一条Next (YYYY-MM-DD)占位条目,表示未发布的下一版本待记录。
八、测试与行为验证
该 crate 的集成测试集中在 crates/filterer/globset/tests/filtering.rs,测试辅助设施(tests/helpers/mod.rs)提供了file_does_pass、dir_doesnt_pass等断言方法,把路径构造、事件打包、check_event调用封装成一行式断言。值得关注的测试族:
- glob 语法语义:精确文件名(
exact_filename)、带目录的精确名(exact_filename_in_folder)、隐藏目录(exact_filename_in_hidden_folder)、*尾缀(glob_single_final_ext_star)、*/与/前缀、**/possum、possum/**、apples/**/oranges等中缀双星模式,正向与忽略两组各有完整对照(如ignore_glob_trailing_double_star与glob_trailing_double_star); - 目录 ignore 与剪枝:裸匹配(
ignore_folder_with_bare_match)、前导斜杠、尾随斜杠(目录专属 glob 不忽略边界文件本身)、**/prunes/**与**/prunes组合(ignore_folder_correctly_with_double_and_double_double_globs)——后者说明"/prunes/不匹配目录边界,遍历仍可能进入"这一细节; - 白名单优先级:
whitelist_overrides_ignore、whitelist_overrides_ignore_files、嵌套白名单(whitelist_overrides_ignore_files_nested),以及路径别名归一化(direct_whitelist_normalises_constructor_and_event_aliases,构造时用first/../watched、事件用second/../watched); - 多路径事件:
multipath_allow_on_any_one_pass验证"任一路径通过即通过"; - 非路径事件:
nonpath_event_passes验证键盘与内部事件必然放行; - 平台特化:Windows 下两个测试(
direct_whitelist_simplifies_verbatim_windows_path等)验证\\?\前缀的 verbatim 路径被dunce::simplified归一化。
附:继续深入
- 过滤器接口定义:crates/lib/src/filter.rs
- 事件与优先级模型:crates/events/src/event.rs
- ignore 文件解析与祖先匹配:crates/ignore-files/src/filter.rs
- ignore 文件自动发现:crates/ignore-files/src/discover.rs
- CLI 组装与命令行参数:crates/cli/src/filterer.rs、crates/cli/src/args/filtering.rs
- 依赖关系与 feature 定义:crates/filterer/globset/Cargo.toml
- 开发工具
- CLI
【免费下载链接】watchexec
Executes commands in response to file modifications
相关推荐
深入解析 watchexec-filterer-ignore:基于 ignore 文件的 Watchexec 过滤组件(v7.0.x 版本演进与源码实现)
深入解析 watchexec filterer ignore:基于 ignore 文件的 Watchexec 过滤组件(v7.0.x 版本演进与源码实现) 导读
开发工具CLIwatchexec-filterer-ignore 深度解析:Watchexec 基于 ignore 文件的子过滤器实现
watchexec filterer ignore 深度解析:Watchexec 基于 ignore 文件的子过滤器实现 本文聚焦 Watchexec 工作区中
开发工具CLIwatchexec ignore-files 演进全解:从 CHANGELOG 到 Rust 忽略文件发现与过滤引擎的源码实现
watchexec ignore files 演进全解:从 CHANGELOG 到 Rust 忽略文件发现与过滤引擎的源码实现 本文以 watchexec 仓库
开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考