Architecture¶
vexy-glob has two layers: a thin Python package that normalises arguments,
and a Rust extension module that walks the file system, filters entries and
searches file contents.
vexy_glob/__init__.py Python API: find, glob, iglob, search, exceptions
vexy_glob/__main__.py CLI (Fire): vg find, vg search
src/lib.rs PyO3 module vexy_glob._vexy_glob: walker, filters, search
src/pattern_cache.rs Compiled glob cache (LRU, 1000 entries)
src/global_init.rs One-time initialisation when the module is imported
src/zero_copy_path.rs Path helpers prepared for later use (not called yet)
src/simd_string.rs String helpers prepared for later use (not called yet)
Python layer¶
vexy_glob.find() does no file-system work itself. It:
- Converts a
Pathroot tostr. - Works out the case mode. With
case_sensitive=None, the glob is case-sensitive if it contains an uppercase letter, and the content regex is checked the same way on its own. - Turns single strings for
extension,excludeandcustom_ignore_filesinto one-element lists. - Converts every time argument to a Unix timestamp (
float). Relative values such as"-2h"are resolved againsttime.time()at this point. - Calls
_vexy_glob.find()or, whencontentis set,_vexy_glob.search(). - Maps errors from the Rust side to
PatternError,SearchErrororVexyGlobErrorby looking at the error message.
glob(), iglob() and search() are small wrappers around find().
Rust core¶
Walking¶
The core builds an ignore::WalkBuilder for the root and configures it from
the arguments:
| Builder setting | Set from |
|---|---|
hidden(!hidden) |
hidden |
ignore, git_ignore, git_exclude |
not ignore_git |
git_global(true) |
always on |
follow_links |
follow_symlinks |
same_file_system |
same_file_system |
max_depth |
max_depth |
threads |
threads, or the CPU count when 0 |
add_ignore(path) |
each existing file in custom_ignore_files, and <root>/.fdignore unless ignore_git |
It then starts the parallel walker (build_parallel()) on a separate thread
and returns to Python.
Filtering¶
Each walker thread runs every entry through one function,
should_include_entry(), which checks in this order: glob pattern, exclude
patterns, file type, extension, size, modification time, access time and
creation time. The first failed check rejects the entry. Size and time checks
read the entry's metadata only when such a filter is set.
Glob patterns are compiled once and kept in a process-wide cache keyed by
pattern and case mode. A pattern with no wildcard characters skips globset
and is compared as a plain string.
Streaming results to Python¶
Accepted paths go into a bounded crossbeam-channel channel. Its capacity
depends on the job:
| Job | Channel capacity |
|---|---|
| Content search | 500 |
Path search with sort |
10,000 |
Path search, threads not set |
1,000 |
Path search, threads=n |
1,000 × n, at most 8,000 |
When Python asks for an iterator, the core wraps the receiving end of the
channel in a VexyGlobIterator object. Each next() call takes one result
from the channel and converts it to a str, a pathlib.Path or, for content
search, a dict. If Python reads slower than the walker produces, the channel
fills up and the walker threads wait, so memory use stays bounded.
When Python asks for a list (as_list=True or sort), the core releases the
GIL, drains the channel while the walker runs, then joins the walker thread,
sorts if requested, and builds a Python list. Releases up to 1.0.9 joined the
walker first and could hang on large result sets; see
Troubleshooting.
Content search¶
For search(), entries that pass should_include_entry() and are regular
files are opened and scanned with a grep_searcher::Searcher and a
grep_regex::RegexMatcher. A custom sink collects one record per matching
line (path, line number, line text) and sends the records for a file to the
channel after the file is finished.
Errors during the walk¶
Walker errors (unreadable directories, a missing root) and file-open errors
travel through the same channel as error records. The iterator prints them to
standard error with the prefix Error during traversal: and moves on. The
list path discards them.
Build and packaging¶
The project is a maturin "mixed" layout (python-source = "." in
pyproject.toml): maturin compiles the crate in src/ into
vexy_glob/_vexy_glob.*.so (or .pyd on Windows) and packages it together
with the Python files in vexy_glob/. PyO3 is built with the abi3-py38
feature, so one wheel per platform works on CPython 3.8 and newer.
The release profile in Cargo.toml uses opt-level = 3, LTO, one codegen
unit, panic = "abort" and stripped symbols.
The package version is the version in Cargo.toml; pyproject.toml
declares it as dynamic. See Contributing for how a
release keeps the git tag and Cargo.toml in step.