Expand description
Compiled, byte-first glob patterns with explicit behavior-changing options.
A Pattern is compiled once from any AsRef<[u8]> and matched many
times against bytes, so filenames never pass through a lossy UTF-8
conversion. Syntax that changes meaning is opt-in through
PatternOptions: recursive **, braces, extglobs, hidden-name matching,
and ASCII case folding are off in PatternOptions::default until asked
for. PatternOptions::walker asks for the first three at once, which is
the dialect the ferralk walker reads its patterns in.
use ferralk_glob::{Pattern, PatternOptions};
let pattern = Pattern::compile("src/**/*.{rs,toml}", PatternOptions::walker())?;
assert!(pattern.is_match_glob_path("src/lib.rs"));
assert!(pattern.is_match_glob_path("src/parser/Cargo.toml"));
assert!(!pattern.is_match_glob_path("src/lib.rs.bak"));Three entry points decide how far a wildcard may reach:
Pattern::is_match_glob_pathkeeps every ordinary wildcard inside one path component, as a shell glob does. Use it for filesystem paths.Pattern::is_matchcompares a whole byte sequence in which/is not special.Pattern::is_match_pathand thePattern::filter_pathsfamily keep zlob’s list-filter rule for callers porting from it.
The same pattern, *.rs, asked through each of them:
| Candidate | is_match | is_match_path | is_match_glob_path |
|---|---|---|---|
lib.rs | yes | yes | yes |
src/lib.rs | yes: * crosses / | yes: a wildcard in the first component crosses | no: * stays in one component |
./lib.rs | no: ./ is compared literally | yes: one leading ./ is ignored | yes: one leading ./ is ignored |
The matcher covers literals, *, ?, **, character
classes, escapes, leading-period handling, ASCII case folding, nested brace
expansion, and Bash-style extglobs. The
usage guide
documents each option with its default.
§Recipes
Every recipe below is a tested example. Finding files on disk is the
ferralk crate’s job; its
recipes cover
.gitignore, include and exclude lists, early termination, errors, and
async use.
§Match a path against several configured globs
A PatternSet is the counterpart of globset::GlobSet: it compiles a
list once and answers whether any glob matches, or which ones do, by
their position in the list. It asks only the globs whose literal
extension, component, prefix or suffix the path has, so a long list costs
little more per path than a short one.
use ferralk_glob::{PatternOptions, PatternSet, PatternSetError};
let globs = ["src/**/*.rs", "tests/**/*.rs", "*.toml"];
let set = PatternSet::new(globs, PatternOptions::walker())?;
assert!(set.is_match_glob_path("src/parser/lexer.rs"));
assert!(set.is_match_glob_path("Cargo.toml"));
// `*` stays in its component, so `*.toml` names the top level only.
assert!(!set.is_match_glob_path("crates/cli/Cargo.toml"));
let mut which = Vec::new();
set.matches_glob_path_into("tests/cli.rs", &mut which);
assert_eq!(which, [1]);PatternSetError::index names the glob that failed to compile. For
globs with different options, collect compiled Patterns into the set
instead; PatternSet shows both.
§Port a fast-glob or globby list with ! negations
A leading ! is not negation here: the matcher reads it as a literal !,
so !**/*.test.ts compiles and matches nothing. Split such a list into
includes and excludes first. fast-glob itself keeps !(…) as a negated
extglob, and so does the split below.
use ferralk_glob::{Pattern, PatternError, PatternOptions, PatternSet};
fn compile_list(globs: &[&str]) -> Result<(PatternSet, PatternSet), PatternError> {
let options = PatternOptions::walker();
let (mut include, mut exclude) = (Vec::new(), Vec::new());
for glob in globs {
match glob.strip_prefix('!').filter(|rest| !rest.starts_with('(')) {
Some(negated) => exclude.push(Pattern::compile(negated, options)?),
None => include.push(Pattern::compile(glob, options)?),
}
}
Ok((include.into_iter().collect(), exclude.into_iter().collect()))
}
let (include, exclude) =
compile_list(&["src/**/*.ts", "!src/**/*.test.ts", "!**/generated/**"])?;
let is_selected =
|path: &str| include.is_match_glob_path(path) && !exclude.is_match_glob_path(path);
assert!(is_selected("src/app/main.ts"));
assert!(!is_selected("src/app/main.test.ts"));
assert!(!is_selected("src/generated/client.ts"));fast-glob’s dot: true is PatternOptions::match_hidden(true), and
its caseSensitiveMatch: false is
PatternOptions::case_insensitive(true). A walker takes the same
list through include and exclude, and rejects a leading ! instead of
reading it literally.
§Match a Path
Patterns take bytes, and path_bytes gives them for a
Path or an OsStr without a lossy
conversion. Match the part of the path the pattern is written for: a
pattern is relative, so strip the directory it is relative to first.
use std::path::Path;
use ferralk_glob::{Pattern, PatternOptions, path_bytes};
let pattern = Pattern::compile("src/**/*.rs", PatternOptions::walker())?;
let root = Path::new("/work/project");
let path = root.join("src").join("bin").join("main.rs");
let relative = path.strip_prefix(root).expect("path is below the root");
assert!(pattern.is_match_glob_path(path_bytes(relative)));
// The unstripped path starts with `/work`, which `src/**` does not.
assert!(!pattern.is_match_glob_path(path_bytes(&path)));A path that ferralk walked already carries its root-relative part as
WalkEntry::relative_path, the spelling the walker’s own patterns match.
§Put literal text into a pattern
A directory name, a path, or user input can contain bytes that are syntax.
escape and escape_str make them literal under every option
combination with escaping enabled, so the text can be joined with glob
syntax of the caller’s own.
use ferralk_glob::{Pattern, PatternOptions, escape_str};
let directory = "reports {2024} [final]";
let glob = format!("{}/**/*.csv", escape_str(directory));
let pattern = Pattern::compile(&glob, PatternOptions::walker())?;
assert!(pattern.is_match_glob_path("reports {2024} [final]/q1/sales.csv"));
assert!(!pattern.is_match_glob_path("reports 2024 f/q1/sales.csv"));
// Unescaped, the braces are an alternation and the brackets a class.
let unescaped = Pattern::compile(format!("{directory}/**/*.csv"), PatternOptions::walker())?;
assert!(unescaped.is_match_glob_path("reports 2024 f/q1/sales.csv"));
assert!(!unescaped.is_match_glob_path("reports {2024} [final]/q1/sales.csv"));§Filter a list of paths
Pattern::filter_paths keeps zlob’s list rule, in which a wildcard in
the first component crosses separators. For shell semantics, filter with
Pattern::is_match_glob_path instead; both are shown on
Pattern::filter_paths.
§Report an invalid pattern
PatternError::offset is a byte offset into the pattern, so a caller
can point at the problem; see PatternError.
Provenance: semantics are ported and differentially checked against zlob
v1.6.3, source commit 4bc4da2cbc823d3911b4a1436448687c398977dd, primarily
zig-src/fnmatch.zig, zig-src/pattern_context.zig, and
test/test_fnmatch.zig. Deliberate differences live in
the checked-in corpus and compatibility matrix.
Structs§
- Pattern
- A compiled glob pattern.
- Pattern
Error - An error returned when a pattern is not syntactically valid.
- Pattern
Options - Explicit switches that affect glob interpretation.
- Pattern
Set - A list of compiled
Patterns matched as one, the counterpart ofglobset::GlobSet. - Pattern
SetError - The glob in a
PatternSet::newlist that failed to compile.
Constants§
- VERSION
- Crate version exposed for build and integration diagnostics.
Functions§
- escape
- Escapes
textso that it compiles to a pattern matching exactlytext. - escape_
str escapefor UTF-8 text, returning aString.- expand_
braces - Expands brace alternatives into the plain patterns a pattern stands for.
- path_
bytes - The bytes a
Patternmatches for a path:OsStr::as_encoded_bytesof anything that isAsRef<OsStr>.