首页
/ nushell nu-glob 源码解析:Unix Shell 风格通配符匹配的 Rust 实现与 dc-glob 实验后端

nushell nu-glob 源码解析:Unix Shell 风格通配符匹配的 Rust 实现与 dc-glob 实验后端

2026-09-05 16:02:42作者:彭桢灵Jeremy

nu-glob 是 Nushell 工作区中负责“把 Unix shell 风格通配符模式匹配到文件系统”的基础 crate,本文基于 crates/nu-glob/README.md 展开,结合 crates/nu-glob/src/lib.rscrates/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)给出了完整语法规则:

  • ? 匹配任意单个字符;
  • * 匹配任意(可为空)字符序列;
  • ** 匹配当前目录及任意层子目录,但它必须独立成一个路径组件——**ab** 都非法,三个及以上连续的 * 也非法;
  • [...] 匹配括号内任一字符,支持按 Unicode 顺序的范围,如 [0-9];未闭合的方括号是非法的;
  • [!...][...] 的否定形式;
  • 元字符本身可用方括号转义,例如 [?];紧跟在 [[! 之后的 ] 被解释为字符集的一部分([]][!]]);- 放在首尾表示字面减号(如 [abc-])。

编译结果是一串 PatternTokenChar / AnyChar / AnySequence / AnyRecursiveSequence / AnyWithin / AnyExcept)。其中有个实用细节:连续多个 ** 组件会被折叠成一个 AnyRecursiveSequencesome/**/**/needle.txtsome/**/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 这类递归展开默认会穿过 .gitnode_modules 之类的隐藏目录。这一行为在迭代器实现里有对应逻辑:遇到递归 pattern 且当前路径是以 . 开头的目录时,只有 recursive_match_hidden_dirtrue 才继续深入(lib.rs)。

require_literal_leading_dot 的效果由一组专门测试固化:开启后 *.txt 不再匹配 .hello.txt,但 .*.* 仍然匹配(test_pattern_matches_require_literal_leading_dot)。require_literal_separator 开启后 abc?defabc*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.rscompiler.rsmatcher.rsglobber.rs 可以看到:

  1. parser:把模式解析为 AST(LiteralStringWildcardRecurseAnyCharacterCharactersAlternativesRepeatSeparator 等节点),测试覆盖了 {a,b,c} 大括号选择、[a-z] 字符类、<*:3> / <*:1,4> 重复计数等经典 glob 不具备的扩展语法;
  2. compiler:把 AST 编译为指令流(compiler.rs 中的 InstructionLiteralStringAnyCharacterAnyStringCharactersJumpAlternativeIncrementBranchIfLessThanComponentBoundaryComplete 等),模式只编译一次,之后可反复用于匹配;
  3. matcher:用编译好的 Program 对路径做匹配,且区分“完整匹配”(valid_as_complete_match)与“有效前缀”(valid_as_prefix)两种结果;
  4. globber:真正遍历文件系统,并在 rayon 后台线程上并行遍历,结果经同步通道(容量 4096)流给消费端迭代器,深度超过 2 层的子树改为内联递归以降低调度开销(PARALLEL_DEPTH_CUTOFF)。

GlobWalkOptions:更强的遍历控制

dc-glob 对外暴露 GlobWalkOptionsmod.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 但不匹配 foobarmatcher_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 目前处于“保留现有行为为默认、可逐项评估新后端”的过渡阶段,README 中的经典 glob/Pattern API 仍是主线。

小结与延伸阅读

nu-glob 以极小的体积承载了 Nushell 文件路径展开的核心能力:跨平台的 Rust 原生 glob 实现、可调的 MatchOptions(并默认让 ** 穿透隐藏目录)、统一区分“模式错误”与“迭代 IO 错误”的结果类型,以及一套对齐 rust-lang/glob 语义、带并行遍历与剪枝优化的 dc-glob 实验后端。关键入口文件:

登录后查看全文
热门项目推荐
相关项目推荐