内容
# 通配符
用于通配符模式匹配、文件系统全局匹配和内容搜索的高性能.NET 10工具包。在热点路径上实现零分配,使用内存映射文件扫描,并提供一个CLI grep工具(`wcg`),其性能与ripgrep相差19%。该工具包附带一个MCP服务器,为AI编码代理提供快速、目录限制的文件搜索和替换功能。
## 要求
- .NET 10.0 或更高版本
## 支持的语法
| 符号 | 描述 |
|-------|-------------|
| `*` | 匹配任意字符序列(包括空序列) |
| `?` | 匹配单个字符 |
| `[abc]` | 匹配字符集中的任意字符 |
| `[a-z]` | 匹配字符范围内的任意字符 |
| `[!x]` 或 `[^x]` | 匹配字符集外的任意字符 |
| `\` | 转义下一个字符(例如 `\*` 匹配一个字面`*`) |
| `{a,b,c}` | 大括号展开 — 匹配用逗号分隔的多个替代项中的任意一个 |
### 仅Glob支持的语法
| 符号 | 描述 |
|-------|-------------|
| `**` | 匹配零个或多个目录级别(递归) |
## 用法
```csharp
// 一次性匹配
bool matches = WildcardPattern.IsMatch("*.cs", "program.cs"); // true
// 预编译模式(在多个输入中重用)
var pattern = WildcardPattern.Compile("file?.log");
pattern.IsMatch("file1.log"); // true
pattern.IsMatch("fileAB.log"); // false
// 不区分大小写的匹配
WildcardPattern.IsMatch("HELLO", "hello", ignoreCase: true); // true
// 大括号交替 — 匹配用逗号分隔的多个替代项中的任意一个
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 — 提取每个`*`捕获的内容
var p = WildcardPattern.Compile("\\[*\\] * - *");
if (p.TryMatch("[2024-03-15] ERROR - timeout", out var captures))
{
// captures: ["2024-03-15", "ERROR", "timeout"]
}
// LINQ扩展
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"
// 转换为Regex以进行互操作
Regex regex = WildcardPattern.Compile("*.csv").ToRegex();
// regex.ToString() == "^.*\\.csv$"
// 转换为Func<string, bool>谓词
PatternPredicate predicate = WildcardPattern.ToPredicate("*.cs");
predicate("app.cs"); // true
predicate("readme.md"); // false
// 批量过滤
var pattern = WildcardPattern.Compile("*.cs");
List<string> csharpFiles = WildcardSearch.FilterLines(pattern, files);
// 大型数据集的并行批量过滤
string[] results = WildcardSearch.FilterBulk(pattern, largeArray, parallel: true);
```
### 文件内容扫描
`FilePathMatcher`使用内存映射I/O扫描磁盘上的文件以匹配通配符模式。
```csharp
// 单个包含模式
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}");
// 多个包含模式 — OR逻辑(匹配包含ERROR或WARN的行)
var matcher = FilePathMatcher.Create(
include: ["*ERROR*", "*WARN*"]
);
var matches = matcher.Scan("app.log");
// 包含/排除模式 — 匹配包含ERROR但不包含DEBUG的行
var matcher = FilePathMatcher.Create(
include: ["*ERROR*"],
exclude: ["*DEBUG*"]
);
var matches = matcher.Scan("app.log");
// 快速布尔检查 — 文件是否包含任何匹配项?
var matcher = FilePathMatcher.Create("*timeout*");
bool hasTimeout = matcher.ContainsMatch("app.log");
// 异步流式传输 — 结果在找到时到达
await foreach (var match in matcher.ScanAsync(filePaths))
Console.WriteLine(match.Line);
// 上下文行 — 类似于grep -C 3(每个匹配项的前3行和后3行)
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}");
}
```
### 文件系统全局匹配
`Glob`匹配磁盘上的文件路径,支持`*`、`?`、`[abc]`、`**`(递归目录匹配)和`{a,b,c}`(大括号展开)。
```csharp
// 递归查找所有.cs文件
var files = Glob.Match("src/**/*.cs").ToList();
// 大括号展开 — 在一个模式中匹配多个扩展名
var webFiles = Glob.Match("**/*.{razor,cs,css}").ToList();
// 单个目录级别
var logs = Glob.Match("/var/log/*.log").ToList();
// 尊重.gitignore — 跳过bin/、obj/、node_modules/、.git/等
var tracked = Glob.Match("**/*.cs", options: new GlobOptions { RespectGitignore = true }).ToList();
// 跟随符号链接(默认关闭,行为与ripgrep一致)
var withSymlinks = Glob.Match("**/*.cs", options: new GlobOptions { FollowSymlinks = true }).ToList();
// 预解析的全局模式以供重用
var glob = Glob.Parse("**/*.json");
foreach (var file in glob.EnumerateMatches("/my/project"))
Console.WriteLine(file);
// 不接触文件系统进行路径匹配
Glob.IsMatch("src/**/*.cs", "src/Models/User.cs"); // true
```
### 查找和替换
`FileReplacer`跨文件执行查找和替换,具有干预预览、原子写入和编码保留。
```csharp
// 预览替换(不修改文件)
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}");
// 应用替换(每个文件的原子写入)
FileReplacer.Apply(filePaths, "ERROR", "WARNING", ignoreCase: true);
// 捕获组替换 — 查找中的通配符,替换中的$1/$2
var results = FileReplacer.Preview(filePaths, "*console.log(*)*", "$1logger.info($2)$3");
// 多行字面查找 — 始终按字面匹配(通配符不能跨行);
// 查找/替换中的行尾根据每个文件的样式标准化
FileReplacer.Apply(filePaths, " DoOld();\n DoOld2();", " DoNew();");
```
安全性:跳过二进制文件、只读文件和超过10MB的文件。保留编码(BOM)和换行符(`\r\n`/`\n`)。通过临时文件+重命名原子写入。如果文件失败(权限、锁定),则报告错误并继续处理其余文件。
### CLI工具 — `wcg`
一个建立在库之上的命令行grep工具。默认情况下,尊重`.gitignore`,流式传输结果。
#### 安装
**选项A — .NET工具**(跨平台,需要.NET 10运行时):
```bash
dotnet tool install -g wcg
dotnet tool update -g wcg # 更新
```
**选项B — 原生二进制文件**(单个文件,无依赖,约4MB):
从[GitHub Releases](https://github.com/Jabe/Wildcard/releases)下载适用于您平台的最新二进制文件:
| 平台 | 二进制文件 |
|----------|--------|
| macOS(Apple Silicon) | `wcg-osx-arm64` |
| Linux(x64) | `wcg-linux-x64` |
| Windows(x64) | `wcg-win-x64.exe` |
```bash
# macOS / Linux
chmod +x wcg-osx-arm64
sudo mv wcg-osx-arm64 /usr/local/bin/wcg
```
#### 用法
```
wcg <glob> [<pattern>...] [options]
参数:
<glob> 文件全局模式(例如 "src/**/*.cs")
<pattern> 内容搜索模式(多个模式使用OR逻辑)— 普通单词作为子字符串匹配;使用通配符进行前缀/后缀/完整模式匹配(例如 "ERROR*", "*.log")。
选项:
-x, --exclude <pattern> 排除匹配模式的行(可重复)
-X, --exclude-path <glob> 排除匹配全局的文件(可重复)
-i, --ignore-case 不区分大小写的内容匹配
-l, --files-with-matches 仅打印包含匹配的文件路径
--no-ignore 不尊重.gitignore文件
-L, --follow 跟随符号链接
-w, --watch 初始扫描后监视更改
-A, --after-context <N> 显示每个匹配项后的N行
-B, --before-context <N> 显示每个匹配项前的N行
-C, --context <N> 显示每个匹配项的前后N行
-c, --count 显示每个文件的匹配行数
--tree 将文件列表渲染为缩进的ASCII树
--depth <N> 树输出的最大目录深度(默认:5)
--all AND模式 — 文件必须包含所有模式
--max-files <N> 输出中要包含的最大文件数
--max-matches <N> 每文件最多显示的匹配项数
-r, --replace <text> 用此字符串替换匹配的内容(干预预览)
--write 将替换写入文件(需要 --replace)
```
示例:
```bash
wcg "src/**/*.cs" # 列出匹配的文件
wcg "src/**/*.cs" --tree # 以ASCII树形式列出匹配的文件
wcg "src/**/*.cs" --tree --depth 2 # 树形结构限制在2级深
wcg "**/*.log" ERROR # 搜索包含ERROR的行
wcg "**/*.log" ERROR WARN # OR模式 — 包含ERROR或WARN
wcg "**/*.log" ERROR WARN --all # AND模式 — 文件必须同时包含两者
wcg "**/*.cs" TODO -x DONE # 搜索TODO,排除DONE
wcg "**/*.cs" TODO FIXME -x DONE # 搜索TODO或FIXME,排除DONE
wcg "**/*.cs" TODO -i # 不区分大小写的搜索
wcg "**/*.log" ERROR --watch # 监视新的ERROR行
wcg "**/*" class -X "*test*" # 搜索,跳过测试路径
wcg "**/*.cs" --no-ignore # 包括.gitignore的文件
wcg "**/*.cs" -L # 跟随符号链接
wcg "**/*.cs" TODO -C 3 # 显示匹配周围的3行上下文
wcg "**/*.log" ERROR -B 2 -A 5 # 匹配前2行,后5行
# 普通单词会自动包装为*word*(子字符串匹配)。
# 在前缀/后缀/模式匹配中使用显式通配符:
wcg "**/*.cs" "using*" # 以"using"开头
wcg "**/*.log" "*.json" # 以".json"结尾
wcg "**/*.log" "*ERROR*timeout*" # 多段通配符
# ? 匹配恰好一个字符:
wcg "**/*.log" "ERR?R" # 匹配ERROR、ERRIR、ERR0R、…
wcg "**/*.log" "v?.?.?" # 匹配v1.2.3、v2.0.1、…
# 字符类:
wcg "**/*.log" "HTTP [45]??" # HTTP 4xx或5xx — [45] + 两个?? 数字
wcg "**/*.log" "[EIWD]*" # 以E、I、W或D开头(ERROR/INFO/WARN/DEBUG)
wcg "**/*.log" "[!D]*" # 不以D开头(排除DEBUG)
# 内容模式中的大括号交替:
wcg "**/*.log" "{ERROR,WARN}*" # 以ERROR或WARN开头
wcg "**/*.cs" "{TODO,FIXME,HACK}*" # 以其中任何一个标记开头
# 输出上限:
wcg "**/*.log" ERROR --max-files 10 # 10个文件后停止
wcg "**/*.log" ERROR --max-matches 5 # 每文件最多显示5个匹配
# 计数模式:
wcg "**/*.cs" TODO -c # 每文件匹配计数 + 汇总
wcg "**/*.cs" TODO -c -l # 仅汇总(总匹配和文件)
wcg "**/*.cs" -c # 匹配文件计数(无内容模式)
# 查找和替换(默认干预预览):
wcg "**/*.cs" oldMethod --replace newMethod # 预览替换
wcg "**/*.cs" oldMethod --replace newMethod --write # 应用更改
wcg "**/*.cs" ERROR --replace WARNING -i # 不区分大小写的替换
# 捕获组替换(查找中的通配符,替换中的$1/$2):
wcg "**/*.cs" "*console.log(*)*" -r '$1logger.info($2)$3' # 重构方法调用
```
### MCP服务器 — `wildcard-mcp`
一个[MCP](https://modelcontextprotocol.io)(模型上下文协议)服务器,将Wildcard的功能暴露为AI编码助手(如Copilot、Claude、Cursor等)的工具。
#### 工具
| 工具 | 描述 |
|------|-------------|
| `wildcard_glob` | 通过全局模式查找文件 — 尊重`.gitignore`,支持计数模式和树形输出 |
| `wildcard_grep` | 搜索文件内容,包含上下文行、计数模式、AND/OR模式、输出上限和并行内存映射I/O。也作为跨平台文件读取器(cat/head) |
| `wildcard_replace` | 跨文件查找和替换 — 默认干预预览,原子写入,捕获组,多行字面查找 |
| `wildcard_peek` | 批量文件读取器 — 在一次调用中读取多个文件,可选行范围和字符预算 |
| `wildcard_watch` | 监视与全局模式匹配的文件更改,持续一段时间 |
#### 安装
```bash
dotnet tool install -g wildcard-mcp
```
#### VS Code / Copilot
添加到 `.vscode/mcp.json`:
```json
{
"servers": {
"wildcard": {
"type": "stdio",
"command": "wildcard-mcp",
"args": ["--live"]
}
}
}
```
#### Claude Code
在项目根目录的 `.mcp.json` 中添加:
```json
{
"mcpServers": {
"wildcard": {
"command": "wildcard-mcp",
"args": ["--live"]
}
}
}
```
服务器通过stdio通信,并自动发现所有五个工具。所有操作都**限制在MCP客户端声明的工作区根目录内** — 路径遍历外部的允许根目录被一个保护程序拒绝,该程序在任何文件I/O之前解析符号链接并验证每个路径。
传递 `--live` 以启用实时模式:服务器在启动时为每个根目录(每个根目录一个)构建内存索引,并通过文件系统监视器保持其最新。全局查询成为内存过滤器,而不是磁盘遍历,这大大加快了大型代码库上的重复搜索。
传递 `--summary` 以在每个工具结果中添加一行调用参数的回声,例如 `[pattern="**/*.cs", ignore_case=true]`。仅显示客户端实际发送的参数,因此您可以审计LLM发送的内容,而无需打开原始请求。默认情况下关闭。
## 基准测试
在Apple M4 Pro、.NET 10.0.7、Arm64 RyuJIT AdvSIMD上进行测量。
### 模式匹配 — Wildcard vs 编译的Regex vs FileSystemName
| 模式 | 输入 | Wildcard | Regex | FSName | 速度提升vs Regex |
|---------|-------|----------|-------|--------|-----------------|
| `*.csv` | 短(15个字符) | 1.3 纳秒 | 13.0 纳秒 | 4.5 纳秒 | **10倍** |
| `*.csv` | 长(74个字符) | 1.3 纳秒 | 15.0 纳秒 | 4.5 纳秒 | **12倍** |
| `*.csv` | 不匹配 | 1.3 纳秒 | 10.2 纳秒 | 4.2 纳秒 | **8倍** |
| `report*.csv` | 短 | 2.4 纳秒 | 13.3 纳秒 | 100.7 纳秒 | **5倍** |
| `report_????.csv` | 短 | 5.3 纳秒 | 9.0 纳秒 | 47.3 纳秒 | **2倍** |
| `[rs]*.*` | 短 | 7.6 纳秒 | 13.3 纳秒 | — | **2倍** |
| `*report*2024*` | 短 | 12.7 纳秒 | 22.0 纳秒 | 236.7 纳秒 | **2倍** |
| `*report*2024*` | 长 | 14.9 纳秒 | 25.4 纳秒 | 1,199.7 纳秒 | **2倍** |
### 真实模式 — Wildcard vs 编译的Regex
| 模式 | 场景 | Wildcard | Regex | 速度提升 |
|---------|----------|----------|-------|---------|
| `v2.*` | 版本前缀 | 0.8 纳秒 | 8.0 纳秒 | **10倍** |
| `[[]2024-03-15*` | 日志日期前缀 | 1.0 纳秒 | 8.1 纳秒 | **8倍** |
| `*@gmail.com` | 电子邮件域名 | 1.5 纳秒 | 10.0 纳秒 | **7倍** |
| `[AEIOU]*`(不区分大小写) | 以元音开头 | 2.4 纳秒 | 6.2 纳秒 | **3倍** |
| `??? *` | 3字符名字 | 3.0 纳秒 | 6.2 纳秒 | **2倍** |
| `J* *Smith`(不区分大小写) | 无匹配 | 4.5 纳秒 | 6.2 纳秒 | **1.4倍** |
| `*ERROR*timeout*` | 日志搜索(无匹配) | 6.9 纳秒 | 8.2 纳秒 | **1.2倍** |
| `*@*.acme-corp.com` | 公司电子邮件 | 7.7 纳秒 | 15.6 纳秒 | **2倍** |
| `SKU-*-BLUE-*` | 产品代码 | 9.3 纳秒 | 10.1 纳秒 | **1.1倍** |
| `J* *Smith`(不区分大小写) | 匹配 | 10.3 纳秒 | 14.7 纳秒 | **1.4倍** |
| `*ERROR*timeout*` | 日志搜索(匹配) | 15.3 纳秒 | 20.0 纳秒 | **1.3倍** |
### 批量过滤 — 10,000 项
| 方法 | 平均值 | 已分配 |
|--------|------|-----------|
| Wildcard FilterLines | 34 微秒 | 33 KB |
| Wildcard FilterBulk (并行) | 79 微秒 | 174 KB |
| FSName LINQ 过滤器 | 67 微秒 | 10 KB |
| Regex LINQ 过滤器 | 200 微秒 | 11 KB |
### 文件内容扫描 — `FilePathMatcher`
模式 `*ERROR*` 跨4个日志文件(~12.5% 匹配行)。与 `File.ReadAllLines` + `FilterLines` 基线比较。
| 文件大小 | 基线(ReadAllLines) | FilePathMatcher | 比例 | 分配比例 |
|-----------|------------------------|-----------------|-------|-------------|
| 小(1K行) | 271 微秒 | 73 微秒 | 0.27 | 0.15 |
| 中(100K行) | 52,220 微秒 | 7,626 微秒 | 0.15 | 0.15 |
| 大(1M行) | 559,739 微秒 | 108,121 微秒 | 0.19 | 0.15 |
### CLI — `wcg` vs `find+grep`,`ripgrep`
真实世界的内容搜索在 `~/Code` 上(~130k 文件,跨多个git仓库)。Apple M4 Pro,.NET 10.0。
| 工具 | 墙上时间 | CPU% |
|------|-----------|------|
| `find+grep` | 17.9秒 | 39% |
| `wcg`(dotnet 工具) | 1.66秒 | 478% |
| `wcg`(原生AOT) | 1.50秒 | 447% |
| `rg`(ripgrep) | **1.26秒** | 316% |
`wcg` 在内容搜索方面与 `ripgrep` 相差 **19%**。默认启用`.gitignore`过滤(默认开启)会修剪`bin/`、`obj/`、`node_modules/`等目录。工作窃取的并行全局重叠了目录枚举和I/O绑定的内容扫描。原生AOT二进制文件增加了约10%的时钟时间改进和10毫秒的冷启动时间。
## 它是如何工作的
1. 模式编译
`PatternCompiler.Compile` 将模式字符串解析为 `Segment` 对象数组。每个分段都是五之一:
- **字** — 固定字符串,(例如 `".cs"`)
- **星号** — `*` 通配符,匹配任意字符序列
- **问号** — `?` 通配符,匹配单个字符
-问号** — 连续的 `?` 字符合并为带有计数
字符类** — 字符集,如 `[a-z]`,转,使用 `SearchValues<char>` 进行 SIMD 加速查找
编译过程中,连续的 `*`会合并为单个星号分段。单个字符、非字符类(例如 `[[]`)会被提升为字面值分段,并与相邻的字面值合并。
**大括号展开 模式中包含 `{a,b,c}`,会在编译前由 `BraceExpander`为多个替代模式。例如,`cs,fs}` `["*.cs", "*.`。每个替代模式都会独立编译,并存储为 `WildcardPattern[]`。如果任意替代成功,则匹配成功。没有的模式会完全跳步骤(零开销) 2. 模式形状优化
在编译时,会检测形状,并分派到快速路径,从而完全绕过引擎:
| 示例 | 快速路径 |
---------|-----------|
| `PureLiteral` | `hello` | `SequenceEqual` |
| `StarSuffix` | `*.csv`EndsWith` |
| `PrefixStar` | `v2 | `StartsWith` `PrefixStarSuffix` |*.csv` | `StartsWith` + `EndsWith` |
| `StarContainsStar` | `*ERROR*` | `IndexOf` |
所有其他模式都会使用通用回溯引擎。
### 3. 匹配引擎
`MatchCore` 使用回溯算法同时遍历分段数组和输入字符串:
- 两个指针跟踪分段和输入中的当前位置。
- 当遇到 `*` 分段时,引擎记录其位置作为点,并推进到下一个分段。
- 如果后续匹配失败,引擎会到最后一个 `*` 位置,并尝试从输入中一个字符。
- ** 加速** — 当 ` 后面跟着一个字面,引擎使用 `Span.IndexOf` 直接跳转到下一个出现位置字符扫描。
- **Ends快速路径** — 当 ` 后面跟着最终字面段时,引擎检查With` 而不是扫描。
这种方法避免了朴素递归实现可能遇到的指数级最坏情况。
### 4. 性能技术
- **零复制匹配** — 使用 `ReadOnlySpan<char>`过程中避免字符串分配。
-内联** — 热点方法如 `MatchLiteral` 和 `Chars 使用 `[MethodImpl(MethodImplgressiveInlining)]` **SIMD 加速字符 — `SearchValues<char>` 提供硬件加速的成员测试。对于大小写的模式,编译展开大小写变体 路径无条件工作。
ref readonly` 结构体 — 热点循环中通过引用访问分段,以避免每次迭代。
- **并行** — `WildcardSearch` 使用 PLINQ处理 1024+ 个,并保留顺序。
###. 文件内容扫描
Matcher` 使用内存/O 和并行处理上的文件:
- **自适应文件 I/O** — 小文件(≤64KB)化缓冲读取;较大文件映射 I/O。超过 2GB 的文件以 重叠部分处理。
字节级预过滤** ASCII、UTF-8 数据上的区分大小写模式,模式在原始字节上运行,使用 SIMD 加速的跨度`IndexOf`、``、`EndsWith`)。的行完全跳过 UTF 解码。当给定多个包含,每个模式都有自己的字节器;仅当所有都拒绝一行时,该行才会被跳过。
-长度门控** — 在解码或匹配之前,小于模式最小可能匹配长度的行。
- **并行多文件扫描** — 通过 `Parallel 并行扫描多个文件后保留文件顺序。
异步流式处理** — `ScanAsync` 使用有界通道在找到匹配项时流式传输它们。
## 致谢
该项目是在 [Anthropic](https://anthropic.com) 的 [Claude Opus 4.6](https://claude.ai) 协助下生成的。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器