Wildcard 2.7.0
See the version list below for details.
dotnet add package Wildcard --version 2.7.0
NuGet\Install-Package Wildcard -Version 2.7.0
<PackageReference Include="Wildcard" Version="2.7.0" />
<PackageVersion Include="Wildcard" Version="2.7.0" />
<PackageReference Include="Wildcard" />
paket add Wildcard --version 2.7.0
#r "nuget: Wildcard, 2.7.0"
#:package Wildcard@2.7.0
#addin nuget:?package=Wildcard&version=2.7.0
#tool nuget:?package=Wildcard&version=2.7.0
Wildcard
A high-performance .NET 10 library for wildcard pattern matching. Provides a lightweight alternative to the full Regex engine, optimized for speed and zero allocations on the hot path.
Requirements
- .NET 10.0 or later
Supported Syntax
| Token | Description |
|---|---|
* |
Matches any sequence of characters (including empty) |
? |
Matches exactly one character |
[abc] |
Matches any character in the set |
[a-z] |
Matches any character in the range |
[!x] or [^x] |
Matches any character NOT in the set |
\ |
Escapes the next character (e.g. \* matches a literal *) |
{a,b,c} |
Brace expansion — matches any of the comma-separated alternatives |
Glob-only Syntax
| Token | Description |
|---|---|
** |
Matches zero or more directory levels (recursive) |
Usage
// One-shot match
bool matches = WildcardPattern.IsMatch("*.cs", "program.cs"); // true
// Pre-compiled pattern (reuse across many inputs)
var pattern = WildcardPattern.Compile("file?.log");
pattern.IsMatch("file1.log"); // true
pattern.IsMatch("fileAB.log"); // false
// Case-insensitive matching
WildcardPattern.IsMatch("HELLO", "hello", ignoreCase: true); // true
// Brace alternation — match any of the comma-separated alternatives
WildcardPattern.IsMatch("*.{cs,fs,vb}", "program.cs"); // true
WildcardPattern.IsMatch("{error,warn}*", "error: timeout"); // true
WildcardPattern.IsMatch("{get,set}_{name,value}", "get_name");// true
// TryMatch — extract what each * captured
var p = WildcardPattern.Compile("\\[*\\] * - *");
if (p.TryMatch("[2024-03-15] ERROR - timeout", out var captures))
{
// captures: ["2024-03-15", "ERROR", "timeout"]
}
// LINQ extensions
string[] files = ["app.cs", "readme.md", "test.cs", "data.csv"];
var csFiles = files.WhereMatch("*.cs").ToList(); // ["app.cs", "test.cs"]
bool hasCsv = files.AnyMatch(WildcardPattern.Compile("*.csv")); // true
string? first = files.FirstMatch(WildcardPattern.Compile("*.md")); // "readme.md"
// Convert to Regex for interop
Regex regex = WildcardPattern.Compile("*.csv").ToRegex();
// regex.ToString() == "^.*\\.csv$"
// Bulk filtering
var pattern = WildcardPattern.Compile("*.cs");
List<string> csharpFiles = WildcardSearch.FilterLines(pattern, files);
// Parallel bulk filtering for large datasets
string[] results = WildcardSearch.FilterBulk(pattern, largeArray, parallel: true);
File Content Scanning
FilePathMatcher scans files on disk for lines matching wildcard patterns using memory-mapped I/O.
// Single include pattern
var matcher = FilePathMatcher.Create("*ERROR*");
List<FilePathMatcher.LineMatch> matches = matcher.Scan("app.log", "server.log");
foreach (var match in matches)
Console.WriteLine($"{match.FilePath}:{match.LineNumber}: {match.Line}");
// Multiple include patterns — OR logic (match lines containing ERROR or WARN)
var matcher = FilePathMatcher.Create(
include: ["*ERROR*", "*WARN*"]
);
var matches = matcher.Scan("app.log");
// Include/exclude patterns — match lines containing ERROR but not DEBUG
var matcher = FilePathMatcher.Create(
include: ["*ERROR*"],
exclude: ["*DEBUG*"]
);
var matches = matcher.Scan("app.log");
// Async streaming — results arrive as they are found
var matcher = FilePathMatcher.Create("*timeout*");
await foreach (var match in matcher.ScanAsync(filePaths))
Console.WriteLine(match.Line);
// Context lines — like grep -C 3 (3 lines before and after each match)
var matcher = FilePathMatcher.Create("*ERROR*");
List<FilePathMatcher.ContextLine> results = matcher.ScanWithContext(
beforeContext: 3, afterContext: 3, "app.log");
foreach (var line in results)
{
var sep = line.IsMatch ? ":" : "-";
Console.WriteLine($"{line.LineNumber}{sep} {line.Line}");
}
File System Globbing
Glob matches file paths on disk with support for *, ?, [abc], ** (recursive directory matching), and {a,b,c} (brace expansion).
// Find all .cs files recursively
var files = Glob.Match("src/**/*.cs").ToList();
// Brace expansion — match multiple extensions in one pattern
var webFiles = Glob.Match("**/*.{razor,cs,css}").ToList();
// Single directory level
var logs = Glob.Match("/var/log/*.log").ToList();
// Respect .gitignore — skips bin/, obj/, node_modules/, .git/ etc.
var tracked = Glob.Match("**/*.cs", options: new GlobOptions { RespectGitignore = true }).ToList();
// Follow symbolic links (off by default, matching ripgrep behavior)
var withSymlinks = Glob.Match("**/*.cs", options: new GlobOptions { FollowSymlinks = true }).ToList();
// Pre-parsed glob for reuse
var glob = Glob.Parse("**/*.json");
foreach (var file in glob.EnumerateMatches("/my/project"))
Console.WriteLine(file);
Find and Replace
FileReplacer performs find-and-replace across files with dry-run preview, atomic writes, and encoding preservation.
// Preview replacements (no files modified)
var results = FileReplacer.Preview(filePaths, "oldMethod", "newMethod");
foreach (var file in results)
foreach (var r in file.Replacements)
Console.WriteLine($"{file.FilePath}:{r.LineNumber}: {r.OriginalLine} → {r.ReplacedLine}");
// Apply replacements (atomic write per file)
FileReplacer.Apply(filePaths, "ERROR", "WARNING", ignoreCase: true);
// Capture-group replacement — wildcards in find, $1/$2 in replace
var results = FileReplacer.Preview(filePaths, "*console.log(*)*", "$1logger.info($2)$3");
Safety: skips binary files, read-only files, and files over 10MB. Preserves encoding (BOM) and line endings (\r\n/\n). Writes atomically via temp file + rename. If a file fails (permissions, locked), the error is reported and the remaining files continue processing.
CLI Tool — wcg
A command-line grep tool built on top of the library. Respects .gitignore by default, streams results as they're found.
Install
Option A — .NET tool (cross-platform, requires .NET 10 runtime):
dotnet tool install -g wcg
dotnet tool update -g wcg # to update
Option B — Native binary (single file, no dependencies, ~4MB):
Download the latest binary for your platform from GitHub Releases:
| Platform | Binary |
|---|---|
| macOS (Apple Silicon) | wcg-osx-arm64 |
| Linux (x64) | wcg-linux-x64 |
| Windows (x64) | wcg-win-x64.exe |
# macOS / Linux
chmod +x wcg-osx-arm64
sudo mv wcg-osx-arm64 /usr/local/bin/wcg
Usage
wcg <glob> [<pattern>...] [options]
Arguments:
<glob> File glob pattern (e.g. "src/**/*.cs")
<pattern> Content search pattern(s) — multiple patterns are OR'd (e.g. ERROR WARN). Plain words match as substrings; use wildcards for prefix/suffix/full patterns (e.g. "ERROR*", "*.log").
Options:
-x, --exclude <pattern> Exclude lines matching pattern (repeatable)
-X, --exclude-path <glob> Exclude files matching glob (repeatable)
-i, --ignore-case Case-insensitive content matching
-l, --files-with-matches Only print file paths that contain matches
--no-ignore Don't respect .gitignore files
-L, --follow Follow symbolic links
-w, --watch Watch for changes after initial scan
-A, --after-context <N> Show N lines after each match
-B, --before-context <N> Show N lines before each match
-C, --context <N> Show N lines before and after each match
-c, --count Show count of matching lines per file
-r, --replace <text> Replace matched content with this string (dry-run preview)
--write Write replacements to files (requires --replace)
Examples:
wcg "src/**/*.cs" # List matching files
wcg "**/*.log" ERROR # Search for lines containing ERROR
wcg "**/*.log" ERROR WARN # OR mode — containing ERROR or WARN
wcg "**/*.cs" TODO -x DONE # Search TODO, exclude DONE
wcg "**/*.cs" TODO FIXME -x DONE # Search TODO or FIXME, exclude DONE
wcg "**/*.cs" TODO -i # Case-insensitive search
wcg "**/*.log" ERROR --watch # Watch for new ERROR lines
wcg "**/*" class -X "*test*" # Search, skip test paths
wcg "**/*.cs" --no-ignore # Include .gitignore'd files
wcg "**/*.cs" -L # Follow symbolic links
wcg "**/*.cs" TODO -C 3 # Show 3 lines of context around matches
wcg "**/*.log" ERROR -B 2 -A 5 # 2 lines before, 5 lines after each match
# Plain words are auto-wrapped as *word* (substring match).
# Use explicit wildcards for prefix/suffix/pattern matching:
wcg "**/*.cs" "using*" # Lines starting with "using"
wcg "**/*.log" "*.json" # Lines ending with ".json"
wcg "**/*.log" "*ERROR*timeout*" # Multi-segment wildcard
# ? matches exactly one character:
wcg "**/*.log" "ERR?R" # Matches ERROR, ERRIR, ERR0R, …
wcg "**/*.log" "v?.?.?" # Matches v1.2.3, v2.0.1, …
# Character classes:
wcg "**/*.log" "HTTP [45]??" # HTTP 4xx or 5xx — [45] + two ?? digits
wcg "**/*.log" "[EIWD]*" # Lines starting with E, I, W or D (ERROR/INFO/WARN/DEBUG)
wcg "**/*.log" "[!D]*" # Lines not starting with D (excludes DEBUG)
# Brace alternation in content patterns:
wcg "**/*.log" "{ERROR,WARN}*" # Lines starting with ERROR or WARN
wcg "**/*.cs" "{TODO,FIXME,HACK}*" # Lines starting with any of those markers
# Count mode:
wcg "**/*.cs" TODO -c # Per-file match counts + summary
wcg "**/*.cs" TODO -c -l # Summary only (total matches and files)
wcg "**/*.cs" -c # Count matching files (no content pattern)
# Find and replace (dry-run preview by default):
wcg "**/*.cs" oldMethod --replace newMethod # Preview replacements
wcg "**/*.cs" oldMethod --replace newMethod --write # Apply changes
wcg "**/*.cs" ERROR --replace WARNING -i # Case-insensitive replace
# Capture-group replacement (wildcards in find, $1/$2 in replace):
wcg "**/*.cs" "*console.log(*)*" -r '$1logger.info($2)$3' # Refactor method calls
Benchmarks
Measured on Apple M4 Pro, .NET 10.0, Arm64 RyuJIT AdvSIMD. Zero allocations for all single-match operations.
Pattern Matching — Wildcard vs Compiled Regex vs FileSystemName
| Pattern | Input | Wildcard | Regex | FSName | Speedup vs Regex |
|---|---|---|---|---|---|
*.csv |
short (15 chars) | 1.3 ns | 13.0 ns | 4.5 ns | 10x |
*.csv |
long (74 chars) | 1.3 ns | 15.0 ns | 4.5 ns | 12x |
*.csv |
no match | 1.3 ns | 10.5 ns | 4.2 ns | 8x |
report*.csv |
short | 2.0 ns | 13.2 ns | 80.0 ns | 7x |
report_????.csv |
short | 5.2 ns | 8.8 ns | 39.1 ns | 2x |
[rs]*.* |
short | 7.5 ns | 13.2 ns | — | 2x |
*report*2024* |
short | 13.0 ns | 21.9 ns | 188.5 ns | 2x |
*report*2024* |
long | 15.2 ns | 25.6 ns | 994.3 ns | 2x |
Real-World Patterns — Wildcard vs Compiled Regex
| Pattern | Scenario | Wildcard | Regex | Speedup |
|---|---|---|---|---|
v2.* |
version prefix | 0.7 ns | 8.3 ns | 11x |
[[]2024-03-15* |
log date prefix | 1.0 ns | 12.3 ns | 12x |
*@gmail.com |
email domain | 1.4 ns | 10.5 ns | 8x |
[AEIOU]* (CI) |
starts with vowel | 2.4 ns | 6.5 ns | 3x |
J* *Smith (CI) |
no match | 2.5 ns | 6.4 ns | 3x |
??? * |
3-char first name | 3.2 ns | 6.4 ns | 2x |
SKU-*-BLUE-* |
product code | 9.5 ns | 10.3 ns | 1.1x |
J* *Smith (CI) |
match | 13.6 ns | 33.2 ns | 2x |
*@*.acme-corp.com |
corporate email | 13.7 ns | 38.5 ns | 3x |
*ERROR*timeout* |
log search (no match) | 19.2 ns | 27.9 ns | 1.5x |
*ERROR*timeout* |
log search (match) | 36.8 ns | 47.6 ns | 1.3x |
Bulk Filtering — 10,000 Items
| Method | Mean | Allocated |
|---|---|---|
| Wildcard FilterLines | 33 µs | 33 KB |
| Wildcard FilterBulk (parallel) | 61 µs | 174 KB |
| FSName LINQ filter | 68 µs | 10 KB |
| Regex LINQ filter | 198 µs | 11 KB |
File Content Scanning — FilePathMatcher
Pattern *ERROR* across 4 log files (~12.5% matching lines). Compared against File.ReadAllLines + FilterLines baseline.
| File size | Baseline (ReadAllLines) | FilePathMatcher | Ratio | Alloc Ratio |
|---|---|---|---|---|
| small (1K lines) | 279 µs | 79 µs | 0.29 | 0.15 |
| medium (100K lines) | 52,811 µs | 7,796 µs | 0.15 | 0.15 |
| large (1M lines) | 558,834 µs | 110,174 µs | 0.20 | 0.15 |
CLI — wcg vs find+grep, ripgrep
Real-world content search on ~/Code (~130k files across multiple git repos). Apple M4 Pro, .NET 10.0.
| Tool | Wall time | CPU% |
|---|---|---|
find+grep |
17.9s | 39% |
wcg (dotnet tool) |
1.66s | 478% |
wcg (native AOT) |
1.50s | 447% |
rg (ripgrep) |
1.26s | 316% |
wcg is within 19% of ripgrep for content search. .gitignore filtering (on by default) prunes bin/, obj/, node_modules/ etc. during traversal. Work-stealing parallel glob overlaps directory enumeration with I/O-bound content scanning. The native AOT binary adds ~10% wall-clock improvement and 10ms cold startup.
How It Works
1. Pattern Compilation
PatternCompiler.Compile parses a pattern string into an array of Segment objects. Each segment is one of five types:
- Literal — a fixed string to match exactly (e.g.
".cs") - Star — the
*wildcard, matches any character sequence - QuestionMark — the
?wildcard, matches exactly one character - QuestionRun — consecutive
?characters collapsed into a single segment with a count - CharClass — a character set like
[a-z], optionally negated, usingSearchValues<char>for SIMD-accelerated lookups
Consecutive * characters are collapsed into a single Star segment during compilation. Single-character, non-negated character classes (e.g. [[]) are promoted to Literal segments and merged with adjacent literals.
Brace expansion — patterns containing {a,b,c} are expanded by BraceExpander into multiple alternative patterns before compilation. For example, *.{cs,fs} becomes ["*.cs", "*.fs"]. Each alternative is compiled independently and stored as a WildcardPattern[]. Matching succeeds if any alternative matches. Patterns without braces skip this step entirely (zero overhead).
2. Pattern Shape Specialization
At compile time, common pattern shapes are detected and dispatched to optimized fast-paths that bypass the general matching engine entirely:
| Shape | Example | Fast-path |
|---|---|---|
PureLiteral |
hello |
SequenceEqual |
StarSuffix |
*.csv |
EndsWith |
PrefixStar |
v2.* |
StartsWith |
PrefixStarSuffix |
report*.csv |
StartsWith + EndsWith |
StarContainsStar |
*ERROR* |
IndexOf |
All other patterns fall through to the general backtracking engine.
3. Matching Engine
MatchCore walks the segment array and the input string simultaneously using a backtracking algorithm:
- Two pointers track the current position in the segments and the input.
- When a
*segment is encountered, the engine records its position as a backtrack point and advances to the next segment. - If a subsequent segment fails to match, the engine backtracks to the last
*position and tries consuming one more character from the input. - IndexOf acceleration — when a
*is followed by a literal, the engine usesSpan.IndexOfto jump directly to the next occurrence instead of scanning character-by-character. - EndsWith fast-path — when a
*is followed by the final literal segment, the engine checksEndsWithinstead of scanning.
This approach avoids the exponential worst-case that naive recursive implementations can hit.
4. Performance Techniques
- Zero-copy matching — uses
ReadOnlySpan<char>to avoid string allocations during matching. - Aggressive inlining — hot-path methods like
MatchLiteralandCharsEqualuse[MethodImpl(MethodImplOptions.AggressiveInlining)]. - SIMD-accelerated character classes —
SearchValues<char>provides hardware-accelerated membership testing. For case-insensitive patterns, both upper and lower case variants are expanded at compile time so the SIMD path works unconditionally. ref readonlystruct access — segments are accessed by reference in the hot loop to avoid copying the struct on each iteration.- Parallel bulk operations —
WildcardSearch.FilterBulkprocesses arrays of 1024+ items in parallel using PLINQ with order preservation.
5. File Content Scanning
FilePathMatcher scans files on disk using memory-mapped I/O and parallel processing:
- Adaptive file I/O — small files (≤64KB) use pooled buffered reads; larger files use memory-mapped I/O. Files over 2GB are processed in 1GB overlapping sections.
- Byte-level pre-filtering — for ASCII, case-sensitive patterns over UTF-8 data, pattern matching runs directly on raw bytes using SIMD-accelerated span operations (
IndexOf,StartsWith,EndsWith). Lines that don't match skip UTF-8 decoding entirely. When multiple include patterns are given, each pattern gets its own byte-level filter; a line is skipped only when all filters reject it. - Minimum length gate — lines shorter than the pattern's minimum possible match length are rejected before any decoding or matching.
- Parallel multi-file scanning — multiple files are scanned concurrently via
Parallel.For, with results merged preserving file order. - Async streaming —
ScanAsyncuses a bounded channel to stream matches as they are found.
Attribution
This project was generated with the assistance of Claude Opus 4.6 by Anthropic.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Wildcard:
| Package | Downloads |
|---|---|
|
Docuvera.Worker.Core
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.5.0 | 281 | 6/16/2026 |
| 3.4.1 | 76 | 6/15/2026 |
| 3.4.0 | 78 | 6/12/2026 |
| 3.3.0 | 72 | 6/12/2026 |
| 3.2.0 | 70 | 6/12/2026 |
| 3.1.1 | 70 | 5/15/2026 |
| 3.1.0 | 195 | 5/5/2026 |
| 3.0.0 | 4,014 | 4/15/2026 |
| 2.11.0 | 81 | 3/25/2026 |
| 2.10.0 | 76 | 3/23/2026 |
| 2.9.0 | 74 | 3/19/2026 |
| 2.8.1 | 82 | 3/18/2026 |
| 2.8.0 | 1,059 | 3/18/2026 |
| 2.7.0 | 77 | 3/18/2026 |
| 2.6.0 | 149 | 3/17/2026 |
| 2.5.0 | 82 | 3/15/2026 |
| 2.4.1 | 74 | 3/15/2026 |
| 2.4.0 | 73 | 3/14/2026 |
| 2.3.0 | 75 | 3/13/2026 |
| 2.2.0 | 74 | 3/13/2026 |