Skip to main content

Crate ferralk_glob

Crate ferralk_glob 

Source
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:

The same pattern, *.rs, asked through each of them:

Candidateis_matchis_match_pathis_match_glob_path
lib.rsyesyesyes
src/lib.rsyes: * crosses /yes: a wildcard in the first component crossesno: * stays in one component
./lib.rsno: ./ is compared literallyyes: one leading ./ is ignoredyes: 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.
PatternError
An error returned when a pattern is not syntactically valid.
PatternOptions
Explicit switches that affect glob interpretation.
PatternSet
A list of compiled Patterns matched as one, the counterpart of globset::GlobSet.
PatternSetError
The glob in a PatternSet::new list that failed to compile.

Constants§

VERSION
Crate version exposed for build and integration diagnostics.

Functions§

escape
Escapes text so that it compiles to a pattern matching exactly text.
escape_str
escape for UTF-8 text, returning a String.
expand_braces
Expands brace alternatives into the plain patterns a pattern stands for.
path_bytes
The bytes a Pattern matches for a path: OsStr::as_encoded_bytes of anything that is AsRef<OsStr>.