news 2026/9/7 8:45:29

uv-globfilter:uv 的 PEP 639 受限 Glob 解析与目录遍历预过滤实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uv-globfilter:uv 的 PEP 639 受限 Glob 解析与目录遍历预过滤实现

uv-globfilter:uv 的 PEP 639 受限 Glob 解析与目录遍历预过滤实现

【免费下载链接】uvAn extremely fast Python package and project manager, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/uv/uv

uv-globfilter是 uv 内部的一个组件 crate,目标是实现"跨语言、跨操作系统"的受限 Glob 语法,并在此基础上提供一个目录遍历预过滤器,用于在walkdir遍历中尽早跳过"内部不可能出现目标文件"的目录。读完本篇,你将掌握 PEP 639 受限 Glob 的完整语法规则、GlobDirFilter基于 DFA 的目录匹配算法、与WalkDir::filter_entry的集成方式,以及它在 uv 构建后端打包license-filessource-include等配置项时的真实用法。

一、定位与动机

crates/uv-globfilter/README.md 对该 crate 的定义是:"Portable directory walking with includes and excludes."(带 include/exclude 的跨平台目录遍历)。其核心动机是:

Motivating example: You want to allow the user to select paths within a project.

即允许用户以 glob 方式在pyproject.toml中声明"哪些路径要包含进构建产物、哪些要排除",例如:

include = ["src", "License.txt", "resources/icons/*.svg"] exclude = ["target", "/dist", ".cache", "*.tmp"]

在遍历目录树时,可以调用

GlobDirFilter::from_globs(...)?.match_directory(&relative)

并在walkdirfilter_entry回调中用它跳过那些"永远不会命中"的目录,从而避免对大目录树的无谓 I/O。这正是"预过滤"的含义:不是判断当前条目本身是否匹配,而是判断当前目录的子树是否还有任何可能匹配 glob 的路径

该 crate 目前仅作为 uv 的内部组件使用(crates/uv-globfilter/Cargo.toml 中description = "This is an internal component crate of uv",版本0.0.76),其公开 API 只有两个类型,外加一个演示用的二进制入口:

  • GlobDirFilter—— 目录遍历预过滤器,见 crates/uv-globfilter/src/glob_dir_filter.rs;
  • PortableGlobParser/PortableGlobError—— PEP 639 受限 glob 的解析器与错误类型,见 crates/uv-globfilter/src/portable_glob.rs;
  • 演示二进制main.rs—— 展示 include/exclude 配合WalkDir的完整用法,见 crates/uv-globfilter/src/main.rs。

crate 的 lib.rs 模块文档明确了设计目标:

The goal is globs that are portable between languages and operating systems.

也就是说,同一个 glob 模式在 Python 工具、Node 工具、Rust 工具中应表现一致,且在 Windows 与类 Unix 系统上一致。为此它选择了 PEP 639 定义的那套受限 glob 语法,并在GlobBuilder层面强制literal_separator(true)(路径分隔符按字面量处理,*不跨目录)。

二、PEP 639 受限 Glob 语法(README 核心规则完整继承)

README 明确声明支持的是PEP 639 的跨语言受限 glob 语法,规则如下(与 portable_glob.rs 中PortableGlobParser::parse的文档注释一一对应):

规则说明
逐字匹配字符字母数字、下划线_、连字符-、点.按字面量匹配
*匹配任意数量的字符,但不跨路径分隔符
?匹配单个字符,不跨路径分隔符
**匹配任意数量的字符,包含路径分隔符(可跨目录)
[...]字符集,仅包含逐字匹配字符;内部连字符-表示与 locale 无关的范围(按 Unicode 码点排序,如a-z);位于开头或结尾的连字符按字面量匹配
路径分隔符固定为斜杠/;模式相对于给定目录,不支持/开头的绝对路径
..父目录指示符不允许出现

README 最后还点出了这条规则链的推论:

These rules mean that matching the backslash (\) is forbidden, which avoids collisions with the windows path separator.

由于/是唯一合法分隔符、反斜杠禁止匹配,Windows 下的反斜杠分隔符问题被从语法层面彻底消除。

uv 扩展变体:PortableGlobParser::Uv

源码中PortableGlobParser是一个两变体的枚举(portable_glob.rs#L64-L73):

pub enum PortableGlobParser { /// Follow the PEP 639 rules strictly. Pep639, /// In addition to the PEP 639 syntax, allow escaping characters with backslashes. /// /// For cross-platform compatibility, escaping path separators is not allowed, i.e., forward /// slashes and backslashes can't be escaped. Uv, }
  • Pep639严格遵循 PEP 639,反斜杠是非法字符;
  • Uv额外允许用反斜杠转义字符(以便匹配@、空格等 PEP 639 不逐字匹配的字符),但出于跨平台兼容,不允许转义/\本身

测试用例 portable_glob.rs#L303-L332 给出了两套合法模式样例,可作为语法参考:

// Pep639 与 Uv 均合法 r"licenses/*.txt" r"licenses/**/*.txt" r"LICEN[CS]E.txt" r"LICEN?E.txt" r"[a-z].txt" r"[a-z._-].txt" r"*/**" r"LICENSE..txt" r"LICENSE_file-1.txt" r"licenses/라이센스*.txt" // 韩文 r"licenses/ライセンス*.txt" // 日文 r"licenses/执照*.txt" // 中文 r"src/**" // 仅 Uv 变体合法(反斜杠转义) r"public-domain/Gulliver\’s\ Travels.txt" r"**/\@test"

三、解析实现:check()前置校验与GlobBuilder构建

PortableGlobParser::parse(portable_glob.rs#L99-L106)的实现分两步:

pub fn parse(&self, glob: &str) -> Result<Glob, PortableGlobError> { self.check(glob)?; Ok(GlobBuilder::new(glob) .literal_separator(true) // No need to support Windows-style paths, so the backslash can be used a escape. .backslash_escape(self.backslash_escape()) .build()?) }

literal_separator(true)保证了*/?不会跨越/**的跨目录语义由globset内部处理。check()(portable_glob.rs#L109-L219)是一台逐字符的状态机,在交给globset之前先做 PEP 639 合规性预校验,覆盖以下细节:

  • 星号数量限制***以及**后紧跟非/(如**license)都会报TooManyStars。源码注释说明原因:这些形式可以用更少的星号等价表示,globcrate 禁止、globset允许、而 PEP 639 文本本身存在歧义,所以这里主动过滤。
  • 父目录检测..出现在字符串开头或/之后才判定为ParentDirectory(如licenses/..位置 9 报错),而LICENSE..txt这类文件名中的连续点是合法的。
  • 字符集校验[...]内部只允许字母数字、_-.;出现其他字符(如!?)报InvalidCharacterRange
  • 反斜杠Pep639模式下任何\都是InvalidBackslashUv模式下\后必须是可转义字符,转义/\InvalidEscapee,结尾悬空报TrailingEscape
  • 其他字符:报InvalidCharacter(Pep639)或InvalidCharacterUv(Uv,多带一条 hint)。

错误类型与快照测试

PortableGlobError共 8 个变体(portable_glob.rs#L7-L46),每种都有带"错误位置 + 原始 glob"的格式化消息。test_error测试(portable_glob.rs#L228-L301)用 insta 快照锁定了代表性错误输出,例如:

The parent directory operator (`..`) at position 0 is not allowed in glob: `..` The parent directory operator (`..`) at position 9 is not allowed in glob: `licenses/..` Invalid character `!` at position 14 in glob: `licenses/LICEN!E.txt` Invalid character `!` in range at position 15 in glob: `licenses/LICEN[!C]E.txt` Too many at stars at position 9 in glob: `licenses/**license` Only forward slashes are allowed as path separator, invalid character at position 8 in glob: `licenses\eula.txt`

Uv 变体还演示了 hint 的呈现(**/@test报错后会附带hint: Characters can be escaped with a backslash),见下一节。

四、GlobDirFilter:从 Glob 到 DFA 的目录预过滤

构建:from_globs

GlobDirFilter持有两个字段(glob_dir_filter.rs#L15-L18):

pub struct GlobDirFilter { glob_set: GlobSet, dfa: Option<dfa::dense::DFA<Vec<u32>>>, }

from_globs(glob_dir_filter.rs#L24-L73)做了三件事:

  1. 正则转换:把每个 glob 的regex()输出去掉(?-u)前缀(glob 本身是逐字节匹配的非 unicode 正则),并把模式中的/替换为平台的MAIN_SEPARATOR,使在 Windows 上运行时能匹配反斜杠路径;
  2. 构建GlobSet:供match_path做精确的"该路径是否匹配"判断;
  3. 构建 dense DFA:用regex_automatadfa::dense::BuilderAnchored起始方式编译所有 glob 正则,并设置dfa_size_limitdeterminize_size_limit,两者共用常量DFA_SIZE_LIMIT = 1_000_000字节(源码注释直言Chosen at a whim)。若 DFA 构建失败(通常是组合爆炸超出限制),则记录warn!并置dfa = None退化为"完整目录遍历"(即match_directory恒返回true),保证功能正确性优先于剪枝效率。

匹配算法:match_directorymatch_path

match_path(glob_dir_filter.rs#L78-L80)是"文件或目录是否匹配任一 glob"的最终判定:

pub fn match_path(&self, path: &Path) -> bool { self.match_directory(path) || self.glob_set.is_match(path) }

match_directory(glob_dir_filter.rs#L86-L120)才是预过滤的核心。它的语义是:"该目录或其任意子孙可能命中"——永不漏判(不会因返回 false 而丢掉实际匹配的子孙),但允许误报(返回 true 但最终没有子孙命中)。算法要点:

  • 根路径(空Path)直接放行;
  • 没有 DFA 时恒返回true(即退化路径);
  • 逐字节把路径喂给 DFA(anchored 起始状态),得到"读到路径末尾"的状态state
  • 计算两个后继状态:
    • eoi_state = next_eoi_state(state):目录自身是否完整匹配某个 glob(例如 glob 是foo/*时,目录foo/bar本身可命中);
    • slash_state = next_state(state, MAIN_SEPARATOR):目录之后还能不能再接路径分量(例如 glob 是foo/bar/*时,目录foo/bar需要继续下钻)。注意源码特意不对slash_statenext_eoi_state,因为要检查的是"还能不能再加字符",而不是"此处是否到达$锚点";
  • 返回is_match_state(eoi_state) || !is_dead_state(slash_state)

这一"自身匹配或可继续下钻"的双条件,恰好与filter_entry的需求吻合:filter_entry只需要知道"该子树值不值得进入"。

测试证据:预过滤确实剪掉了分支

prefilter测试(glob_dir_filter.rs#L164-L218)在临时目录里构造了 5 条path*/dir*/subdir/a.txt文件链,配合 5 个代表性模式:

const PATTERNS: [&str; 5] = [ "path1/*", // 只需下钻一级 "path2/dir2", // 只需下钻一级 "path3/dir3/subdir/a.txt", // 精确到文件 "path4/**/*", // 需要完整下钻 "path5", // 只匹配目录本身,无需下钻 ];

断言结果显示WalkDir实际访问的条目为:

""、path1、path1/dir1、 path2、path2/dir2、 path3、path3/dir3、path3/dir3/subdir、path3/dir3/subdir/a.txt、 path4、path4/dir4、path4/dir4/subdir、path4/dir4/subdir/a.txt、 path5 ← 注意:path5/dir5 及更深层完全未被访问

path5只匹配目录自身,match_directory("path5/dir5")返回false,于是filter_entry剪掉了整个子树——这正是"目录永远不会再匹配"的跳过语义。同文件的walk_dir测试(glob_dir_filter.rs#L221-L282)进一步验证:在filter_entrymatch_directory剪枝、对留下的条目用match_path做最终筛选后,得到的文件集合与预期完全一致,即预过滤不会造成漏选

五、与WalkDir的集成范式(main.rs演示)

crates/uv-globfilter/src/main.rs 是一个可运行的参考实现,展示了 README 中 motivating example 的完整落地方式:

let includes = ["src/**", "pyproject.toml"]; let excludes = ["__pycache__", "*.pyc", "*.pyo"]; // include:用 PortableGlobParser::Pep639 解析后交给 GlobDirFilter let include_matcher = GlobDirFilter::from_globs(include_globs).unwrap(); // exclude:构造 unanchored GlobSet let mut exclude_builder = GlobSetBuilder::new(); for exclude in excludes { // Excludes are unanchored let exclude = if let Some(exclude) = exclude.strip_prefix("/") { exclude.to_string() } else { format!("**/{exclude}").to_string() }; let glob = PortableGlobParser::Pep639.parse(&exclude).unwrap(); exclude_builder.add(glob); } let exclude_matcher = exclude_builder.build().unwrap();

两个值得注意的设计:

  1. exclude 是"非锚定"的:不带前导/的排除模式会被自动包上**/前缀,因此exclude = ["__pycache__"]会排除任意深度的__pycache__目录;而"/dist"这类带前导/的模式(去掉/后)只匹配相对根目录的顶层路径。这与 README 示例中exclude = ["target", "/dist", ".cache", "*.tmp"]的写法完全对应。

  2. 两级过滤:在WalkDir上先以

    include_matcher.match_directory(&relative) && !exclude_matcher.is_match(&relative)

    filter_entry(剪枝),再对幸存条目做

    if !include_matcher.match_path(&relative) || exclude_matcher.is_match(&relative) { continue; }

    做最终包含/排除判定。剪枝阶段宁松勿漏,最终阶段才精确裁决。

注意:演示二进制的includes/excludes是硬编码的(当前仓库中如此),它的作用是在开发期验证过滤行为,生产代码(如 uv-build-backend)则从pyproject.toml读取同构的配置。

六、在 uv 构建后端中的真实应用

uv-globfilter目前唯一的下游使用方是 crates/uv-build-backend(uv 的 PEP 517 构建后端实现),有三条调用链,分别对应不同的配置项与 parser 变体。

6.1project.license-files(PEP 639):严格Pep639变体

PEP 639 定义了license-files字段用于声明许可证文件 glob。uv 在构建 source dist 与 wheel 时都消费它:

  • 源分发包:source_dist.rs#L138-L147 中,pyproject_toml.license_files_source_dist()返回的每个 glob 都用PortableGlobParser::Pep639.parse(...)解析,错误上下文标记为project.license-files
  • wheel:wheel.rs#L208-L225 中,wheel 构建把命中的许可证文件写入{dist-info-name}/{version}.dist-info/licenses/目录(通过wheel_subdir_from_globs,内部同样使用Pep639变体与GlobDirFilter::from_globs);
  • 元数据:metadata.rs#L712-L729 中,wheel 的METADATA也按 PEP 639 变体解析license_files,且 metadata.rs#L589-L599 显示"一旦检测到project.license-files(或license = { text = ... }SPDX 表达式),METADATA 版本即提升到 2.4"。

因为这是标准字段,必须严格遵循 PEP 639,故使用Pep639变体而非Uv变体。

6.2tool.uv.build-backend.source-include:宽松Uv变体

自定义源包含是 uv 的扩展配置,允许反斜杠转义以便匹配带特殊字符的文件名,因此使用Uv变体。source_dist.rs#L114-L122:

for include in includes { let glob = PortableGlobParser::Uv .parse(&include) .map_err(|err| Error::PortableGlob { field: "tool.uv.build-backend.source-include".to_string(), source: err, })?; include_globs.push(glob); }

配置项文档(settings.rs#L51-L60)给出的官方示例即:

[tool.uv.build-backend] source-include = ["tests/**"]

同一段构建流程中,数据目录(tool.uv.build-backend.data.<name>)也以"{dir}/**"形式经Uv变体解析后加入 include globs(source_dist.rs#L149-L171);而模块目录与pyproject.toml、readme 始终被强制包含。最终所有 include globs 经GlobDirFilter::from_globs合并为唯一include_matcher(source_dist.rs#L180-L184),与本文第五节的main.rs范式一致。

6.3source-exclude与默认排除

排除侧的逻辑在 source_dist.rs#L186-L200:

  • default-excludes默认true,内置默认排除为__pycache__*.pyc*.pyo(settings.rs#L62-L70);
  • 用户source-exclude与默认值合并去重后构造exclude_matcher
  • 有一个硬性校验:若排除规则命中pyproject.toml,直接报错Error::PyprojectTomlExcluded,错误文案见 lib.rs#L59:"pyproject.tomlmust not be excluded from source distribution build"),因为源分发包没有pyproject.toml就无法再被构建。

排除模式在build_exclude_matcher中同样遵循main.rs展示的"非锚定"约定(自动补**/前缀),且这些排除对 source dist 与 wheel 生效,保证"从源树直接构 wheel"与"先构 sdist 再构 wheel"产物一致(settings.rs#L72-L79 的注释明确了这一点)。

七、错误提示设计:Hint与构建后端的呈现

PortableGlobError实现了uv_errors::Hinttrait(portable_glob.rs#L48-L57):只有InvalidCharacterUv变体会附带 hint——"Characters can be escaped with a backslash"。这是刻意针对Uv变体的:当用户写了 PEP 639 不支持逐字匹配的字符(如@),提示应引导其使用反斜杠转义,而不是直接失败。

构建后端如何呈现这条 hint,可在 lib.rs#L513-L528 的快照测试format_err_renders_portable_glob_hints中验证:

Unsupported glob expression in: tool.uv.build-backend.source-include Caused by: Invalid character `@` at position 3 in glob: `**/@test` hint: Characters can be escaped with a backslash

外层Error::PortableGlob携带出错配置项的字段名,内层PortableGlobError携带位置与原始 glob,配合 hint 形成"字段 + 原因 + 修复建议"三层信息。

八、小结与边界说明

  • 语法边界uv-globfilter实现的是 PEP 639 的受限glob 子集——不支持绝对路径(无前导/)、不支持..、不支持反斜杠路径分隔符;Uv变体是唯一合法的反斜杠用途(转义非分隔符字符)。
  • 预过滤语义match_directory是"可能包含"判断,存在误报、不存在漏报;真正的包含判定必须由match_path完成。两者配合WalkDir::filter_entry与最终filter_map使用,glob_dir_filter.rs的测试保证了该组合下文件集合的正确性。
  • 性能边界:DFA 大小上限为 1,000,000 字节,超限后退化为完整遍历(正确性不变,仅失去剪枝收益);globset层面的GlobSet构建失败则由调用方(如source-include场景)包装为GlobSetTooLarge错误向用户报错。
  • 当前适用范围:截至当前仓库版本(uv-globfilter 0.0.76),该 crate 仅被uv-build-backend使用,服务于 sdist/wheel 构建中的文件选择;仓库中未见其他 crate 依赖它(除自身main.rs演示外)。

如需深入,建议按以下顺序阅读源码:crates/uv-globfilter/src/portable_glob.rs(语法与错误)、crates/uv-globfilter/src/glob_dir_filter.rs(DFA 预过滤)、crates/uv-globfilter/src/main.rs(集成范式),再到 crates/uv-build-backend/src/source_dist.rs 与 crates/uv-build-backend/src/wheel.rs(生产调用链)。

【免费下载链接】uvAn extremely fast Python package and project manager, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/uv/uv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:44:17

豆包工作Agent实战拆解:从任务拆解到工具调用的Agent落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:42:07

堆外内存 OOM:现象分析与优化方案

目录 一、现象分析 (一)内存使用率不断上升 (二)GC 时间飙升 (三)线程被 Block (四)RES 超过 -Xmx 设置 二、堆外内存 OOM 的原因 (一)堆外内存泄漏的主要原因 1. 主动申请未释放 2. JNI 调用的 Native Code 申请的内存未释放 (二)堆外内存泄漏的排查策略…

作者头像 李华
网站建设 2026/9/7 8:41:35

2026年论文初稿提速攻略:AI写作工具怎么选才不踩雷

引言&#xff1a;初稿这件事&#xff0c;卡住了多少人的时间 论文初稿的撰写周期往往被低估。多数人计划两周完成&#xff0c;实际却拖到一个月以上。问题不在写作能力&#xff0c;而在于从选题到成稿的各个环节缺乏高效的组织方式。选题方向反复摇摆、文献梳理耗时过长、框架…

作者头像 李华
网站建设 2026/9/7 8:37:26

阵列天线方向图比较:从直线阵到共形阵的工程选型要点

简介&#xff1a;在无线通信与雷达系统中&#xff0c;阵列天线的方向图直接影响增益、覆盖与抗干扰能力。这份资源以均匀直线阵为例&#xff0c;将单元个数、阵元间距与波长三个核心参数对方向图的影响&#xff0c;整理为4个MATLAB脚本&#xff08;压缩包仅2KB&#xff0c;文件…

作者头像 李华
网站建设 2026/9/7 8:37:23

ARM Compiler v6.16 32位在Keil MDK中的迁移与优化

简介&#xff1a;面向 STM32/Keil 开发者的 ARM Compiler 6.16 离线安装包&#xff0c;专门解决 Keil 中编译器未正确安装、版本缺失或不匹配导致的编译报错。资源提供官方 standalone 32 位版本&#xff0c;适合在 Windows 主机上为 Keil MDK 补装编译器&#xff0c;安装后可在…

作者头像 李华