Ferralk 1.2, a glob matcher and file walker for Rust
Find files. Skip whole folders.Name the folders, and the rest stays closed.
Every build tool, linter and indexer starts by finding files: all TypeScript sources, every test, everything except the dependencies. You describe them with a glob pattern such as src/**/*.ts. Ferralk is a Rust library that matches those patterns and walks the folders on disk to find them, in parallel. It reads the pattern before it walks, so folders that cannot hold a match are never opened. For build tools, linters, and anything that starts with a file list.
- 1,015recorded cases, each naming where its answer comes from
- Bytesfile names matched as stored, never converted to text
- 2 cratesthe walker, and the matcher on its own
- Rust 1.96+MIT or Apache-2.0, no unsafe code in the matcher
One line that describes a set of files.
Before a build tool compiles anything, before a linter checks a single line, it has to know which files to look at. Nobody lists them one by one. You write a pattern instead: src/**/*.ts means every TypeScript file anywhere under src. Such patterns are called globs, and you have typed them in a terminal, in a .gitignore and in the settings of nearly every tool you use.
Globs look simple and hide a few decisions. Does a star reach into subfolders? Does it match a name that starts with a period? Ferralk answers the way a shell does: a single star stays inside one folder, the double star is the explicit way to go deeper, and a wildcard leaves a leading period alone unless you ask. Switch between the patterns below to see which of the same five paths each one selects.
main.tsselectedsrc/main.tsselectedsrc/main.test.tsselectedsrc/components/button.tsxnot selecteddocs/guide.mdnot selected
Real output of ferralk-glob 1.1.0, which checks each path the way the walker reads a pattern. The double star stands for any number of folders, including none.
Let the pattern narrow the search.
Most file walkers open every folder and filter what they find. Ferralk reads the pattern first and leaves the folders closed that cannot hold a match.
A JavaScript project may hold a few thousand source files beside a node_modules folder with tens of thousands more. A walker that lists everything and filters afterwards pays for every one of those folders, even when the question was only about src. The pattern {src,packages}/**/*.ts already says where the answer can be: under src or under packages, and nowhere else.
Ferralk takes the folder names a pattern fixes and starts its walk there. Ignore rules and excludes close more folders on the way, so an ignored node_modules stays shut whatever the pattern says. Choose a search below and compare what it finds with what it runs into.
let result = Walker::new(".").include("{src,packages}/**/*.{ts,tsx}")?.respect_git_ignore(true).options(WalkOptions::default().files_only(true).sort(true)).collect()?;
packages/ui/index.tssrc/components/button.tsxsrc/main.ts
Errors, from result.errors()
None.
Real output of ferralk 1.1.0 in a small project built for this page. Its pgdata folder cannot be read, like a database folder that belongs to another user. A search that has to look inside reports that next to its result; a search whose pattern names other folders never tries.
Show the project’s files and its .gitignore
src/main.tssrc/components/button.tsxpackages/ui/index.tspublic/legacy.tsnode_modules/vendor/index.tsdist/main.js.cache/old.tspgdata/base.ts
node_modules/dist/
A file name is a row of bytes.
Most names are valid text. The ones that are not still belong to real files, and Ferralk keeps them intact.
To your file system, a name is a sequence of bytes. Most of them spell valid UTF-8 text, but old archives, mounted disks and other systems bring names in older encodings such as Latin-1. A program that turns such a name into text has to replace what it cannot read, and the replacement character looks the same for every unreadable byte.
Ferralk matches the bytes themselves, on every platform: the raw bytes on Unix and a lossless encoding of the name on Windows. A path the walker returns is a path you can open again, and two different files keep two different names.
| File name | Bytes on disk | As lossy text | *.txt | café.txt | caf\xE9.txt |
|---|---|---|---|---|---|
café.txtUTF-8 | 63 61 66 C3 A9 2E 74 78 74 | café.txt | selected | selected | not selected |
caf\xE9.txtLatin-1 | 63 61 66 E9 2E 74 78 74 | caf�.txt | selected | not selected | selected |
caf\xE8.txtLatin-1 | 63 61 66 E8 2E 74 78 74 | caf�.txt | selected | not selected | not selected |
Real output of ferralk-glob 1.1.0, matching the names as bytes in memory. \xE9 is one byte that is not valid UTF-8. The lossy column is what a program sees after Rust’s String::from_utf8_lossy: two different files, one name.
One engine for the search. Your policy around it.
Build tools, linters and indexers need a file list they can explain. Every choice the walker makes has a documented default and a switch that changes it.
- Git ignores, when you ask
- Opt in to .gitignore and .ignore files. Nested rules and Git’s own settings are checked against Git itself.
- Parallel or one by one
- Collect with a thread budget, take entries as they arrive, or decide about each entry with your own function while the walk runs in parallel.
- Errors next to the result
- A folder that cannot be read is reported beside the files, as in the search above. You can skip such errors instead, or stop at the first one.
- The pattern language you mean
- Wildcards stay inside one folder, as in a shell. The double star, braces and extglob groups are on in the walker and opt-in in the matcher.
Less walking shows up in the time.
Where the pattern names its folders, Ferralk finished well ahead of ignore, zlob and jwalk in the published run. Where every folder has to be read, it still came first, by a smaller margin.
Finding files comes first in almost every build, lint and test run, and a tool that watches for changes repeats it again and again. Its cost grows with everything in the project, including the folders nobody asked about.
The chart compares the same searches over the same project: a few thousand sources beside a large node_modules. Switch between a search that names its folders, a search through every folder, and matching a single path.
Time for the same work
Ferralk = 1×, the line through every row. A bar past it took longer.
The published comparison also covers walkdir, globwalk, wax, Linux and the macOS-native backend. Every arm, its command and its limits
Show the figures as a table
| Task | Tool | Time | Relative |
|---|---|---|---|
| Search named folders | Ferralk | 4.32 ms | 1× |
| Search named folders | ignore (Rust, 4 threads, pruning written by hand) | 7.61 ms | 1.76× |
| Search named folders | zlob (Zig, 4 threads) | 22.08 ms | 5.11× |
| Search named folders | jwalk + globset (Rust, 4 threads) | 23.3 ms | 5.39× |
| Search every folder | Ferralk | 20.83 ms | 1× |
| Search every folder | jwalk + globset (Rust, 4 threads) | 23.15 ms | 1.11× |
| Search every folder | zlob (Zig, 4 threads) | 30.09 ms | 1.44× |
| Search every folder | ignore (Rust, 4 threads, globs as overrides) | 38.07 ms | 1.83× |
| Match one path | Ferralk | 11 ns | 1× |
| Match one path | zlob (Zig) | 35 ns | 3.18× |
| Match one path | globset (Rust) | 38 ns | 3.45× |
| Match one path | fast-glob (Rust) | 99 ns | 9.00× |
Held to a frozen reference.
Glob matching is full of small decisions. Ferralk writes each one down as a case, together with the tool its answer comes from.
Ferralk learns from zlob, a fast glob library written in Zig. Its matching and walking behavior was recorded from zlob 1.6.3 and frozen, so a new zlob release can never move the expectations. The implementation is independent: plain Rust with its own API and no C interface.
Every behavior the project promises lives in a checked-in collection of 1,015 cases in 20 topics, and each case names where its expected answer comes from. 48 of them record a deliberate difference from the reference, each with the decision record that explains it. CI replays every case on every pull request, and a pinned Git replays the ignore cases.
- Recorded from zlob 1.6.3435
- Checked by Git’s own check-ignore126
- Shared with fast-glob, where the syntax overlaps47
- Written by hand for a policy or a past bug407
Counted from the corpus folder of the repository. How the verification works
A focused engine. Your tool around it.
Ferralk is a glob matcher and file walker. It matches glob patterns and walks file trees in parallel, ahead of globset and fast-glob. It finds the files and leaves the work on them to your program.
- Search the disk
- Add the walker with
cargo add ferralk. It includes the matcher. - Match paths you already have
- Add only the matcher with
cargo add ferralk-glob, without the walker’s dependencies. - TypeScript applications
- Palamedes, an i18n toolchain for TypeScript apps, runs on Ferralk. There is no npm package of Ferralk itself.
- Not included
- Reading the files it finds, watching them for changes, or a command-line tool. Ferralk hands your program the list.
Every build tool pays for finding files before it does any work. Ferralk keeps zlob's byte-first approach in pure Rust, without Zig or a C ABI, and holds its matcher and walker to a frozen zlob reference. It is one of the Ferramenta engines, made by Sebastian Software in Mainz, Germany.
Start with the file list.
Add the walker to search the disk, or only the matcher for paths you already hold. Both read the glob patterns your users already write.
GitHub repositoryDocumentationAPI referenceIntegration and support