A fast, cross-platform glob pattern matching API for finding files and directories. Implements POSIX.2 glob specification.
Glob patterns provide a concise, human-readable syntax for matching file and directory paths — the same wildcard notation used
by Unix shells, .gitignore files, and build systems. This repository provides two .NET packages for working with glob
patterns:
Both implement the POSIX.2 glob specification with extensions for Windows and Unix-like systems, including environment variable expansion and platform-aware case sensitivity.
$HOME, %USERPROFILE%, and ~IEnumerable-based streaming of resultsIFileSystem abstraction for unit testing without touching the diskUsing the dotnet CLI:
dotnet add package vm2.Glob.Api
From Visual Studio Package Manager Console:
Install-Package vm2.Glob.Api
For the companion command-line tool, see vm2.GlobTool.
using vm2.Glob.Api;
var enumerator = new GlobEnumerator
{
Glob = "**/*.cs",
FromDirectory = "./src",
};
foreach (var path in enumerator.Enumerate())
Console.WriteLine(path);| Pattern | Meaning | Example |
|---|---|---|
* | Any sequence of characters (except path separator) | *.txt matches file.txt |
? | Any single character | file?.txt matches file1.txt |
[abc] | Any character in set | [abc].txt matches a.txt |
[a-z] | Any character in range | [0-9].txt matches 5.txt |
[!abc] | Any character NOT in set | [!.]*.txt excludes hidden files |
** | Zero or more directory levels (globstar) | **/test/**/*.cs — recursive |
[:class:] | Named character class (alpha, digit, lower, upper, etc.) | [[:digit:]]*.log |
Clone the GitHub repository. The library source is in the src/Glob.Api directory.
git clone https://github.com/vmelamed/vm2.Glob.git
cd vm2.GlobCommand line:
dotnet build
Visual Studio / VS Code:
The test projects are in the test directory. They use MTP (Microsoft Testing Platform) with xUnit. Tests are buildable and
runnable from the command line and from Visual Studio Code across operating systems.
Command line:
dotnet test
The tests can also be run standalone after building:
dotnet build
test/Glob.Api.Tests/bin/Debug/net10.0/Glob.Api.Tests
The benchmark project is in the benchmarks/Glob.Api.Benchmarks directory. It uses BenchmarkDotNet.
Command line:
dotnet run --project benchmarks/Glob.Api.Benchmarks/Glob.Api.Benchmarks.csproj -c Release
Standalone after building:
dotnet build -c Release benchmarks/Glob.Api.Benchmarks/Glob.Api.Benchmarks.csproj
benchmarks/Glob.Api.Benchmarks/bin/Release/net10.0/Glob.Api.Benchmarks
Create a GlobEnumerator, set the pattern and starting directory, then call Enumerate():
var enumerator = new GlobEnumerator
{
Glob = "**/*.cs",
FromDirectory = "./src",
};
foreach (var file in enumerator.Enumerate())
Console.WriteLine(file);The GlobEnumeratorBuilder provides a fluent API for configuring and creating an enumerator in a single expression:
var results = new GlobEnumeratorBuilder()
.WithGlob("**/*Tests.cs")
.FromDirectory("./test")
.SelectFiles()
.CaseSensitive()
.Build()
.Configure(new GlobEnumerator())
.Enumerate()
.ToList();Or use Create() to get a pre-configured enumerator directly:
var enumerator = new GlobEnumeratorBuilder()
.WithGlob("**/*.cs")
.FromDirectory("./src")
.SelectFiles()
.Build()
.Create();
foreach (var file in enumerator.Enumerate())
Console.WriteLine(file);Register GlobEnumerator with your application's DI container using the provided extension methods:
// In Startup.cs or Program.cs — register with default FileSystem
services.AddGlobEnumerator();
// In your service — resolve a configured enumerator
public class FileService(IServiceProvider sp)
{
public IEnumerable<string> FindFiles(string pattern)
=> sp.GetGlobEnumerator(b => b.WithGlob(pattern).SelectFiles())
.Enumerate();
}The builder exposes the full range of enumerator options:
var enumerator = new GlobEnumeratorBuilder()
.WithGlob("**/docs/**/*.md")
.FromDirectory("/usr/share")
.SelectFiles()
.CaseInsensitive()
.DepthFirst()
.Distinct() // remove duplicates from multi-globstar patterns
.Build()
.Configure(new GlobEnumerator());
foreach (var file in enumerator.Enumerate())
ProcessFile(file);By default, the enumerator skips hidden and system files. On Unix-like systems this also excludes dotfiles
(e.g., .gitignore). Set AttributesToSkip to None to include everything:
var enumerator = new GlobEnumerator
{
Glob = "**/*",
FromDirectory = "./src",
AttributesToSkip = FileAttributes.None, // include all files
};// Skip only temporary files
enumerator.AttributesToSkip = FileAttributes.Temporary;
// Skip multiple attributes
enumerator.AttributesToSkip = FileAttributes.Hidden
| FileAttributes.System
| FileAttributes.Temporary;// Throw on inaccessible files (strict mode)
enumerator.IgnoreInaccessible = false;
try
{
foreach (var file in enumerator.Enumerate())
ProcessFile(file);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Access denied: {ex.Message}");
}
// Skip inaccessible files silently (default, permissive mode)
enumerator.IgnoreInaccessible = true;var enumerator = new GlobEnumerator
{
Glob = "*",
FromDirectory = "./src",
Enumerated = Objects.Directories,
ReturnSpecialDirectories = true, // include "." and ".."
};Note:
ReturnSpecialDirectoriesis rarely needed and defaults tofalsefor cleaner results.
enumerator.Enumerated = Objects.Files; // files only (default)
enumerator.Enumerated = Objects.Directories; // directories only
enumerator.Enumerated = Objects.FilesAndDirectories; // bothenumerator.MatchCasing = MatchCasing.PlatformDefault; // insensitive on Windows, sensitive on Unix (default)
enumerator.MatchCasing = MatchCasing.CaseSensitive; // always case-sensitive
enumerator.MatchCasing = MatchCasing.CaseInsensitive; // always case-insensitiveenumerator.DepthFirst = false; // breadth-first (default) — process siblings before children
enumerator.DepthFirst = true; // depth-first — fully explore each subtree before moving onenumerator.Distinct = false; // allow duplicates (default, faster)
enumerator.Distinct = true; // remove duplicates (uses a HashSet internally)Note: Deduplication is only necessary for patterns with multiple globstars (e.g.,
**/docs/**/*.md) that may enumerate the same path more than once.
public IEnumerable<string> GetSourceFiles(string projectPath)
{
var enumerator = new GlobEnumeratorBuilder()
.WithGlob("**/*.cs")
.FromDirectory(projectPath)
.SelectFiles()
.Build()
.Configure(new GlobEnumerator());
return enumerator.Enumerate()
.Where(f => !f.Contains("/obj/") && !f.Contains("/bin/"));
}public IEnumerable<string> FindTestAssemblies(string artifactsPath)
{
var enumerator = new GlobEnumerator
{
Glob = "**/*Tests.dll",
FromDirectory = artifactsPath,
Enumerated = Objects.Files,
};
return enumerator.Enumerate();
}public void CleanupLogs(string logDirectory, int daysOld)
{
var cutoff = DateTime.Now.AddDays(-daysOld);
var enumerator = new GlobEnumerator
{
Glob = "**/*.log",
FromDirectory = logDirectory,
};
foreach (var logFile in enumerator.Enumerate())
{
if (File.GetLastWriteTime(logFile) < cutoff)
File.Delete(logFile);
}
}public Dictionary<string, string> LoadConfigurations(string configPath)
{
var enumerator = new GlobEnumeratorBuilder()
.WithGlob("**/appsettings*.json")
.FromDirectory(configPath)
.SelectFiles()
.CaseInsensitive()
.Build()
.Configure(new GlobEnumerator());
return enumerator.Enumerate()
.ToDictionary(
f => Path.GetFileName(f),
f => File.ReadAllText(f)
);
}The library provides an IFileSystem abstraction so that code depending on GlobEnumerator can be tested without touching the
file system. The repository includes a ready-made FakeFileSystem in the test/Glob.Api.FakeFileSystem project, but you can
also supply your own implementation:
public class InMemoryFileSystem : IFileSystem
{
// Implement: IsWindows, GetFullPath, GetCurrentDirectory,
// DirectoryExists, FileExists,
// EnumerateDirectories, EnumerateFiles
}
// Pass the custom file system to the enumerator
var enumerator = new GlobEnumerator(new InMemoryFileSystem())
{
Glob = "**/*.cs",
FromDirectory = "/src",
};
var results = enumerator.Enumerate().ToList();src/**/*.cs is faster than **/*.cs because the search starts deeper in the tree.Objects.Files avoids directory-enumeration overhead when you only need files.** increases traversal depth; avoid patterns like **/a/**/b when a/**/b suffices.HashSet has a memory cost proportional to the result count.IEnumerable, not materialized into a list.Span<T> and stackalloc internally for pattern parsing and transformation.Distinct is enabled, a HashSet<string> tracks every returned path.Typical performance on GitHub Actions Ubuntu Runner, e.g.
BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.103
[Host] : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v4
DefaultJob : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v4| Method | Pattern | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| 'Get Files' | **/* | 65.22 us | 0.711 us | 0.665 us | baseline | 4.2725 | 106.3 KB | ||
| 'Get Directories' | **/* | 30.71 us | 0.180 us | 0.168 us | 2.12x faster | 0.02x | 1.9531 | 48.52 KB | 2.19x less |
| 'Get Files and Directories' | **/* | 67.88 us | 0.497 us | 0.465 us | 1.04x slower | 0.01x | 4.2725 | 107.52 KB | 1.01x more |
| 'Get Files' | **/test/**/* | 48.82 us | 0.286 us | 0.268 us | baseline | 3.0518 | 75.18 KB | ||
| 'Get Directories' | **/test/**/* | 39.58 us | 0.588 us | 0.550 us | 1.23x faster | 0.02x | 2.5024 | 61.43 KB | 1.22x less |
| 'Get Files and Directories' | **/test/**/* | 47.59 us | 0.420 us | 0.393 us | 1.03x faster | 0.01x | 3.0518 | 75.38 KB | 1.00x more |
| 'Small File System' | **/*.cs | 142.3 us | 0.60 us | 0.56 us | baseline | 7.8125 | 195.91 KB | ||
| 'Large File System' | **/*.cs | 235.9 us | 1.50 us | 1.40 us | 1.66x slower | 0.01x | 12.6953 | 314.2 KB | 1.60x more |
| 'Small File System' | **/*.md | 125.2 us | 0.65 us | 0.61 us | baseline | 6.5918 | 165.97 KB | ||
| 'Large File System' | **/*.md | 178.6 us | 0.89 us | 0.83 us | 1.43x slower | 0.01x | 9.2773 | 231.65 KB | 1.40x more |
| 'Traverse Depth First' | **/*.cs | 147.47 us | 0.367 us | 0.344 us | baseline | 7.8125 | 195.91 KB | ||
| 'Traverse Breadth First' | **/*.cs | 149.77 us | 0.413 us | 0.386 us | 1.02x slower | 0.00x | 7.8125 | 195.91 KB | 1.00x more |
| 'Traverse Depth First' | **/docs/**/*.md | 79.12 us | 0.215 us | 0.191 us | baseline | 4.5166 | 111.21 KB | ||
| 'Traverse Breadth First' | **/docs/**/*.md | 78.58 us | 0.498 us | 0.441 us | 1.01x faster | 0.01x | 4.5166 | 111.21 KB | 1.00x more |
| Method | Pattern | Mean | Error | StdDev | Gen0 | Allocated | |||
|---|---|---|---|---|---|---|---|---|---|
| 'Pattern Complexity' | *.md | 7.590 us | 0.0287 us | 0.0254 us | 0.3586 | 8.91 KB | |||
| 'Pattern Complexity' | **/?????Service.cs | 144.493 us | 0.9531 us | 0.8915 us | 9.0332 | 225.32 KB | |||
| 'Pattern Complexity' | **/*.cs | 144.943 us | 0.9319 us | 0.8261 us | 7.8125 | 195.91 KB | |||
| 'Pattern Complexity' | **/*.md | 125.416 us | 0.7815 us | 0.7310 us | 6.5918 | 165.97 KB | |||
| 'Pattern Complexity' | **/docs/**/*.md | 81.249 us | 0.3774 us | 0.3345 us | 4.5166 | 111.21 KB | |||
| 'Pattern Complexity' | **/te(...)ts.cs [22] | 81.770 us | 0.2898 us | 0.2711 us | 4.6387 | 115.69 KB | |||
| 'Pattern Complexity' | **/test/**/*.cs | 75.597 us | 0.4659 us | 0.4358 us | 4.2725 | 107.59 KB | |||
| 'Pattern Complexity' | src/*.cs | 8.991 us | 0.0357 us | 0.0334 us | 0.4120 | 10.16 KB |
Legends:
GlobEnumerator(IFileSystem? fileSystem = null, ILogger<GlobEnumerator>? logger = null)Both parameters are optional. When fileSystem is null, the enumerator uses the real file system.
| Property | Type | Default | Description |
|---|---|---|---|
Glob | string | "" (treated as "*") | The glob pattern to match. |
FromDirectory | string | "." (current directory) | Starting directory for enumeration. |
Enumerated | Objects | Files | Files, Directories, or FilesAndDirectories. |
MatchCasing | MatchCasing | PlatformDefault | PlatformDefault, CaseSensitive, or CaseInsensitive. |
DepthFirst | bool | false | true for depth-first; false for breadth-first. |
Distinct | bool | false | Remove duplicate paths from results. |
ReturnSpecialDirectories | bool | false | Include "." and ".." entries. |
IgnoreInaccessible | bool | true | Skip entries that throw access-denied exceptions. |
AttributesToSkip | FileAttributes | Hidden | System | Skip entries with these file attributes. |
| Method | Returns | Description |
|---|---|---|
Enumerate() | IEnumerable<string> | Execute the glob and stream matches. |
All builder methods return the builder instance for method chaining.
| Method | Description |
|---|---|
WithGlob(string pattern) | Set the glob pattern. |
FromDirectory(string path) | Set the starting directory. |
SelectFiles() | Enumerate files only. |
SelectDirectories() | Enumerate directories only. |
SelectDirectoriesAndFiles() | Enumerate both. |
Select(Objects type) | Set object type explicitly. |
CaseSensitive() | Case-sensitive matching. |
CaseInsensitive() | Case-insensitive matching. |
PlatformSensitive() | Platform-default case sensitivity. |
WithCaseSensitivity(MatchCasing casing) | Set case sensitivity explicitly. |
DepthFirst() | Depth-first traversal. |
BreadthFirst() | Breadth-first traversal (default). |
TraverseDepthFirst(TraverseOrder order) | Set traversal order explicitly. |
Distinct() | Enable deduplication. |
WithDistinct(bool distinct) | Set deduplication explicitly. |
IncludeSpecialDirectories(bool include = true) | Include "." and ".." entries. |
SkipInaccessible(bool skip = true) | Skip access-denied entries. |
SkipObjectsWithAttributes(FileAttributes attrs) | Skip entries with specified attributes. |
Build() | Finalize the builder (returns this). |
Create() | Build and return a new configured GlobEnumerator. |
Configure(GlobEnumerator enumerator) | Apply settings to an existing GlobEnumerator. |
// Register GlobEnumerator with default FileSystem
services.AddGlobEnumerator();
// Register with a builder configuration
services.AddGlobEnumerator(b => b.SelectFiles().CaseSensitive());
// Resolve a configured enumerator from the service provider
var enumerator = serviceProvider.GetGlobEnumerator(
b => b.WithGlob("**/*.cs").FromDirectory("./src"));Have a feature you'd like to see? Open an issue or upvote an existing request. The Votes column reflects community interest and helps prioritize development.
| Votes | Feature | Syntax | Description | Status |
|---|---|---|---|---|
| 10 | Brace expansion | {a,b,c} | Expand comma-separated alternatives: *.{cs,fs} matches both *.cs and *.fs | ❌ |
| 8 | Exclusion patterns | !pattern or --exclude | Exclude paths matching a pattern, e.g. **/*.cs with !**/obj/** | ❌ |
| 6 | Multiple patterns | repeated args or -p | Accept several patterns in one invocation: glob "**/*.cs" "**/*.fs" | ❌ |
| 4 | Max depth limit | --max-depth N | Restrict how deep ** can descend | ❌ |
| 0 | Backslash escaping | \*, \?, \[ | Escape special characters with \ instead of bracket notation [*] | ❌ |
| 0 | Numeric ranges | {1..10} | Generate a sequence of numbers as part of brace expansion | ❌ |
| 0 | Extglob — optional | ?(pattern) | Match zero or one occurrence of the pattern | ❌ |
| 0 | Extglob — one-or-more | +(pattern) | Match one or more occurrences | ❌ |
| 0 | Extglob — zero-or-more | *(pattern) | Match zero or more occurrences | ❌ |
| 0 | Extglob — exactly one | @(a|b) | Match exactly one of the pipe-delimited alternatives | ❌ |
| 0 | Extglob — negation | !(pattern) | Match anything except the pattern | ❌ |
| 0 | Alternation | (a|b) | Inline alternatives without full brace expansion | ❌ |
| Votes | Feature | Syntax | Description | Status |
|---|---|---|---|---|
| 0 | Min depth limit | --min-depth N | Skip results shallower than N levels | ❌ |
| 0 | Dotglob mode | --dotglob | Let * and ** match leading dots without including system files | ❌ |
| 0 | Follow symlinks | --follow-links | Follow symbolic links during traversal | ❌ |
| 0 | Null-delimited output | -0, --print0 | Use \0 as delimiter (safe for filenames with spaces) | ❌ |
| 0 | Count-only mode | --count | Print only the number of matches | ❌ |
| 0 | Regex fallback | r:pattern prefix | Allow a raw regex when glob syntax is insufficient | ❌ |
| 0 | File metadata filters | --newer, --larger | Post-match filters on age, size, etc. | ❌ |
MIT — See LICENSE
See CHANGELOG.md for version history and release notes.