nushell nu-glob 源码解析:Unix Shell 风格通配符匹配的 Rust 实现与 dc-glob 实验后端
nu-glob 是 Nushell 工作区中负责“把 Unix shell 风格通配符模式匹配到文件系统”的基础 crate,本文基于 crates/nu-glob/README.md 展开,结合 crates/nu-glob/src/lib.rs 与 crates/nu-glob/src/dc_glob 的源码,讲解它的 API、模式语法、匹配选项与错误模型,以及正在评估中的 dc-glob 实验后端如何改变 Nushell 的 glob 展开行为。
nu-glob 是什么:Rust 重写的 glob
README 对它的定义只有一句话:Support for matching file paths against Unix shell style patterns(支持将文件路径与 Unix shell 风格模式匹配)。
从 crates/nu-glob/src/lib.rs 的模块文档可以看到更完整的定位:
glob/glob_with函数用于查询文件系统中所有匹配某个模式的文件(类似 libc 的glob);Pattern类型提供单个路径字符串是否匹配模式的能力(类似 libc 的fnmatch);- 为了跨平台一致性和 Windows 支持,整个模块完全用 Rust 实现,而不是委托给 libc 的
glob/fnmatch。
crates/nu-glob/Cargo.toml 中将其描述为 “Fork of glob”,即源自 Rust 社区的 glob crate(版权头同时保留了 The Rust Project 与 The Nushell Project 双方),并做了 Nushell 所需的定制,例如默认匹配选项的调整(后文详述)和 dc-glob 实验后端。
快速上手:README 中的依赖方式与示例
README 给出的使用方式如下。在 Cargo.toml 中添加依赖:
[dependencies]
nu-glob = "0.60.0"
然后用 glob 函数递归打印 /media/ 及其所有子目录下的 jpg 文件:
use nu_nu_glob::glob;
for entry in glob("/media/**/*.jpg").expect("Failed to read glob pattern") {
match entry {
Ok(path) => println!("{:?}", path.display()),
Err(e) => println!("{:?}", e),
}
}
需要注意,README 示例反映的是这个 fork 早期的 API。对照当前仓库源码,glob 的签名已经演化为 lib.rs 中的:
pub fn glob<I: Interruptible>(pattern: &str, interrupt: I) -> Result<Paths<I>, PatternError>
第二个参数是一个 Interruptible 句柄,用于在迭代过程中周期性检查是否需要取消遍历(例如用户按 Ctrl+C)。不需要取消时传入单元结构 Uninterruptible 即可:
use nu_glob::{glob, Uninterruptible};
for entry in glob("/media/**/*.jpg", Uninterruptible).expect("Failed to read glob pattern") {
match entry {
Ok(path) => println!("{:?}", path.display()),
Err(e) => println!("{:?}", e),
}
}
此外,crate 的测试入口中通过 doctest!("../README.md") 宏(lib.rs)把 README 代码块注册为文档测试,保证示例与实现不脱节。
当前公开 API 全景
围绕 README 的 glob 示例,源码提供了四个入口函数和一组工具函数:
| API | 作用 |
|---|---|
glob(pattern, interrupt) |
使用默认 MatchOptions::default() 展开模式,等价于 glob_with(pattern, MatchOptions::default(), interrupt) |
glob_with(pattern, options, interrupt) |
使用自定义匹配选项展开模式;文档明确 require_literal_separator 在展开场景下恒为 true |
glob_with_parent(pattern, options, parent, interrupt) |
把相对模式挂到指定父目录下,主要供测试使用,方便多线程测试在不同目录并行跑 |
is_glob(pattern) / is_glob_with_backend(pattern) |
判断字符串是否含 glob 元字符(*、?、[);后者根据实验选项 dc-glob 选择后端 |
escape_with_backend(pattern) |
转义 glob 元字符使路径按字面匹配,同样按实验选项选择后端 |
返回值是 Paths<I> 迭代器,产出 GlobResult(即 Result<PathBuf, GlobError>),文档强调:路径按字母序产出(Paths are yielded in alphabetical order)。迭代器内部维护一个 todo 待办栈和一个 scope(模式的前缀目录),首次 next() 时才填充待办栈,把“模式解析失败”与“迭代中途 IO 失败”两类错误统一区分开。
模式语法:Pattern 支持的元字符
Pattern::new 是模式编译器,其文档注释(lib.rs)给出了完整语法规则:
?匹配任意单个字符;*匹配任意(可为空)字符序列;**匹配当前目录及任意层子目录,但它必须独立成一个路径组件——**a、b**都非法,三个及以上连续的*也非法;[...]匹配括号内任一字符,支持按 Unicode 顺序的范围,如[0-9];未闭合的方括号是非法的;[!...]是[...]的否定形式;- 元字符本身可用方括号转义,例如
[?];紧跟在[或[!之后的]被解释为字符集的一部分([]]、[!]]);-放在首尾表示字面减号(如[abc-])。
编译结果是一串 PatternToken(Char / AnyChar / AnySequence / AnyRecursiveSequence / AnyWithin / AnyExcept)。其中有个实用细节:连续多个 ** 组件会被折叠成一个 AnyRecursiveSequence(some/**/**/needle.txt 与 some/**/needle.txt 等价),源码测试 test_recursive_wildcards 验证了这一点,同时验证了 ** 可以作为模式开头、/ 开头的 /**/test 也合法。
非法模式的报错通过 PatternError { pos, msg } 表达,携带错误的大致字符位置,例如 Pattern::new("a/**b") 会返回 pos == 4(见 test_wildcard_errors)。Pattern::escape 则把 ?、*、[、] 包进方括号,保证转义后的字符串只字面匹配自己(! 不需要转义,因为它只在方括号内有特殊含义)。
匹配侧提供四组方法:matches / matches_with(对 &str)和 matches_path / matches_path_with(对 &Path,内部先转 str,非 UTF-8 路径按现有 FIXME 约定暂不支持)。
MatchOptions:四个开关与 Nushell 的默认值调整
MatchOptions 结构体(lib.rs)控制匹配行为,四个字段含义如下:
| 字段 | 含义 |
|---|---|
case_sensitive |
是否大小写敏感。目前仅考虑 ASCII 大小写关系,未来可能扩展到 Unicode |
require_literal_separator |
路径分隔符是否必须由字面 / 匹配,而不允许被 *、?、[...] 吞掉 |
require_literal_leading_dot |
以 . 开头的组件(Unix 隐藏文件)是否必须由模式中的字面 . 匹配 |
recursive_match_hidden_dir |
若模式含 **,是否允许 ** 匹配隐藏目录,例如 true 时 ** 可匹配 .abcdef/ghi |
值得注意的是,这个 fork 改写了 Default 实现:
impl Default for MatchOptions {
fn default() -> Self {
Self {
case_sensitive: true,
require_literal_separator: false,
require_literal_leading_dot: false,
recursive_match_hidden_dir: true, // 与原 glob crate 不同
}
}
}
recursive_match_hidden_dir 默认为 true,意味着 Nushell 中 ls **/*.rs 这类递归展开默认会穿过 .git、node_modules 之类的隐藏目录。这一行为在迭代器实现里有对应逻辑:遇到递归 pattern 且当前路径是以 . 开头的目录时,只有 recursive_match_hidden_dir 为 true 才继续深入(lib.rs)。
require_literal_leading_dot 的效果由一组专门测试固化:开启后 *.txt 不再匹配 .hello.txt,但 .*.* 仍然匹配(test_pattern_matches_require_literal_leading_dot)。require_literal_separator 开启后 abc?def、abc*def 甚至 abc[/]def 都不能再匹配 abc/def(对应测试)。
错误模型:GlobError 与迭代器语义
README 示例中 for entry in glob(...) 循环里的 Err(e) 分支对应的就是 GlobError:当某个已匹配的目录无法读取(通常是权限不足)时,迭代器返回 Err(GlobError { path, error }),并携带 path()、error() 访问器和 into_error() 解构方法。测试 test_iteration_errors 用非 root 用户读 /root/* 的场景验证了这一点:IO 错误不会中断迭代,只是作为单个 Err 项产出。
如果想忽略读不了的路径,文档给出的惯用写法是:
use nu_glob::{glob, Uninterruptible};
use std::result::Result;
for path in glob("/media/pictures/*.jpg", Uninterruptible).unwrap().filter_map(Result::ok) {
println!("{}", path.display());
}
fill_todo 里还藏着一个性能优化:如果某个 pattern 组件没有任何元字符(pattern_as_str 能把它还原成纯字符串),迭代器跳过 read_dir,直接对那一个条目做 metadata 探测并递归,只有真正含通配符的组件才触发目录枚举。
dc-glob:正在评估的新实验后端
当前源码中最值得关注的演进是 crates/nu-glob/src/dc_glob 子模块——一个由 Devyn Cairns 从 glob_experiment 引入、用于在 Nushell 中评估的实验性 glob 后端。它与经典实现的最大差别在于架构:经典 Pattern 是 token 序列 + 递归回溯匹配,而 dc-glob 是解析器 → 编译器 → 虚拟机式匹配器三段式。
四阶段流水线
从 dc_glob/mod.rs 与同目录的 parser.rs、compiler.rs、matcher.rs、globber.rs 可以看到:
- parser:把模式解析为 AST(
LiteralString、Wildcard、Recurse、AnyCharacter、Characters、Alternatives、Repeat、Separator等节点),测试覆盖了{a,b,c}大括号选择、[a-z]字符类、<*:3>/<*:1,4>重复计数等经典 glob 不具备的扩展语法; - compiler:把 AST 编译为指令流(compiler.rs 中的
Instruction:LiteralString、AnyCharacter、AnyString、Characters、Jump、Alternative、Increment、BranchIfLessThan、ComponentBoundary、Complete等),模式只编译一次,之后可反复用于匹配; - matcher:用编译好的
Program对路径做匹配,且区分“完整匹配”(valid_as_complete_match)与“有效前缀”(valid_as_prefix)两种结果; - globber:真正遍历文件系统,并在 rayon 后台线程上并行遍历,结果经同步通道(容量 4096)流给消费端迭代器,深度超过 2 层的子树改为内联递归以降低调度开销(
PARALLEL_DEPTH_CUTOFF)。
GlobWalkOptions:更强的遍历控制
dc-glob 对外暴露 GlobWalkOptions(mod.rs),比经典 MatchOptions 多了面向遍历的选项:
pub struct GlobWalkOptions {
pub max_depth: Option<usize>, // 最大递归深度,None 为不限制
pub follow_symlinks: bool, // 遍历时是否跟随目录符号链接
pub excludes: Vec<String>, // 排除的 glob 模式,用于过滤并剪枝
pub interrupt: Option<Arc<AtomicBool>>, // 中断标志,置位后尽快停止遍历
pub ignore_case: bool, // 路径组件大小写不敏感(仅 ASCII)
}
配套函数为 glob_from(相对基准目录展开)、glob_from_interruptible(可取消版本)和 glob_with(带完整选项版本)。excludes 的典型用法在测试 glob_with_respects_excludes_and_prunes_nested_dirs 中:用 **/target/**、**/.git/**、**/node_modules/** 排除目录后,**/* 的遍历会直接剪掉这些子树而不进入。
对常见形态还有“递归后缀快速路径”优化(detect_recursive_suffix_fast_path):像 crates/**/mod*.rs 这种“静态前缀 + ** + 纯尾段模式”的写法,遍历尾段时只做廉价的 basename 匹配(RecursiveFastPath::Suffix / BasenamePattern),对应测试 detect_fast_path_for_recursive_richer_basename_pattern。
另外 DcPattern 类型允许“编译一次、匹配多次”:DcPattern::new / with_ignore_case 构造后,matches_path 可在不做任何文件系统遍历的前提下对任意路径做完整匹配——这是经典 Pattern::matches 的等价物,但走 dc-glob 的编译产物。
语义对齐 rust-lang/glob
dc-glob 的递归语义被明确对齐到 rust-lang/glob 的行为,这一点写在了实验选项的描述里(dc_glob.rs):
- 裸
**和dir/**匹配目录本身(包括模式前缀/起始目录); **/*匹配起始目录之下的条目(不含起始目录自身);- 额外的
/*段强制最小深度(**/*/*至少两个组件)。
这些规则在 matcher 测试中有回归断言,例如 foo/** 完整匹配 foo 但不匹配 foobar(matcher_prefixed_trailing_double_star_matches_directory_itself),*/* 不能匹配单组件路径(回归 issue #18600)。
实验选项:dc-glob 如何被 Nushell 启用
dc-glob 并非默认行为,而是通过 Nushell 的实验选项机制 opt-in(crates/nu-experimental/src/options/dc_glob.rs):
- 标识符:
dc-glob,状态OptIn,自 0.112.3 引入,关联 issue 18101; - 开启后,
is_glob_with_backend与escape_with_backend(lib.rs)会改走 dc-glob 的实现; - 命令层同样分支:crates/nu-command/src/filesystem/glob.rs 的
glob命令、ls相关的 crates/nu-engine/src/glob_from.rs 以及du、watch等命令内部都通过nu_experimental::DC_GLOB.get()决定走哪条后端; - 测试侧用
#[exp(nu_experimental::DC_GLOB)]注解为两套后端分别维护断言,如 crates/nu-command/tests/commands/glob.rs 与ls.rs中的多组用例;benches/benchmarks.rs 里也提供了切换 DC_GLOB 开关跑对比基准的辅助逻辑。
也就是说,从源码结构看,dc-glob 目前处于“保留现有行为为默认、可逐项评估新后端”的过渡阶段,README 中的经典 glob/Pattern API 仍是主线。
小结与延伸阅读
nu-glob 以极小的体积承载了 Nushell 文件路径展开的核心能力:跨平台的 Rust 原生 glob 实现、可调的 MatchOptions(并默认让 ** 穿透隐藏目录)、统一区分“模式错误”与“迭代 IO 错误”的结果类型,以及一套对齐 rust-lang/glob 语义、带并行遍历与剪枝优化的 dc-glob 实验后端。关键入口文件:
- crates/nu-glob/README.md:crate 定位、依赖方式与示例;
- crates/nu-glob/src/lib.rs:
glob/glob_with/Pattern/MatchOptions/GlobError及全部行为测试; - crates/nu-glob/src/dc_glob/mod.rs:dc-glob 对外 API、
GlobWalkOptions与覆盖解析/编译/匹配/遍历的完整测试集; - crates/nu-experimental/src/options/dc_glob.rs:
dc-glob实验选项定义与语义说明; - crates/nu-engine/src/glob_from.rs:Nushell 引擎中 glob 展开的实际调用点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00