中的glob模式匹配.NET
本文关键字:NET 模式匹配 glob 中的 | 更新日期: 2023-09-27 17:48:52
中是否有内置机制。NET来匹配正则表达式以外的模式?我想使用UNIX样式(glob)通配符(*=任意字符的任意数目)进行匹配。
我想将其用于面向最终用户的控制。我担心允许RegEx的所有功能将非常令人困惑。
我喜欢我的代码更具语义,所以我写了这个扩展方法:
using System.Text.RegularExpressions;
namespace Whatever
{
public static class StringExtensions
{
/// <summary>
/// Compares the string against a given pattern.
/// </summary>
/// <param name="str">The string.</param>
/// <param name="pattern">The pattern to match, where "*" means any sequence of characters, and "?" means any single character.</param>
/// <returns><c>true</c> if the string matches the given pattern; otherwise <c>false</c>.</returns>
public static bool Like(this string str, string pattern)
{
return new Regex(
"^" + Regex.Escape(pattern).Replace(@"'*", ".*").Replace(@"'?", ".") + "$",
RegexOptions.IgnoreCase | RegexOptions.Singleline
).IsMatch(str);
}
}
}
(更改名称空间和/或将扩展方法复制到您自己的字符串扩展类)
使用这个扩展,您可以编写这样的语句:
if (File.Name.Like("*.jpg"))
{
....
}
只是为了让代码更清晰:-)
只是为了完整性。自2016年以来,dotnet core
中出现了一个名为Microsoft.Extensions.FileSystemGlobbing
的新nuget包,它支持高级全局路径。(Nuget包)
例如,搜索通配符嵌套的文件夹结构和文件,这在web开发场景中非常常见。
wwwroot/app/**/*.module.js
wwwroot/app/**/*.js
这与.gitignore
文件用于确定从源代码管理中排除哪些文件的方法有些相似。
我为您找到了实际的代码:
Regex.Escape( wildcardExpression ).Replace( @"'*", ".*" ).Replace( @"'?", "." );
列表方法的2参数和3参数变体(如GetFiles()
和EnumerateDirectories()
)将搜索字符串作为支持文件名globbing的第二个参数,同时支持*
和?
。
class GlobTestMain
{
static void Main(string[] args)
{
string[] exes = Directory.GetFiles(Environment.CurrentDirectory, "*.exe");
foreach (string file in exes)
{
Console.WriteLine(Path.GetFileName(file));
}
}
}
将产生
GlobTest.exe
GlobTest.vshost.exe
文档指出,对于匹配的扩展,有一些注意事项。它还指出,8.3个文件名是匹配的(可能是在幕后自动生成的),这可能会在给定的某些模式中导致"重复"匹配。
支持此功能的方法有GetFiles()
、GetDirectories()
和GetFileSystemEntries()
。Enumerate
变体也支持这一点。
如果你想避免正则表达式,这是一个基本的glob实现:
public static class Globber
{
public static bool Glob(this string value, string pattern)
{
int pos = 0;
while (pattern.Length != pos)
{
switch (pattern[pos])
{
case '?':
break;
case '*':
for (int i = value.Length; i >= pos; i--)
{
if (Glob(value.Substring(i), pattern.Substring(pos + 1)))
{
return true;
}
}
return false;
default:
if (value.Length == pos || char.ToUpper(pattern[pos]) != char.ToUpper(value[pos]))
{
return false;
}
break;
}
pos++;
}
return value.Length == pos;
}
}
这样使用:
Assert.IsTrue("text.txt".Glob("*.txt"));
如果使用VB.Net,则可以使用类似Glob语法的Like语句。
http://www.getdotnetcode.com/gdncstore/free/Articles/Intoduction%20to%20the%20VB%20NET%20Like%20Operator.htm
我已经为编写了一个globbing库。NETStandard,具有测试和基准测试。我的目标是为制作一个图书馆。NET,具有最小的依赖性,不使用Regex,并且性能优于Regex。
你可以在这里找到:
- github.com/dazinator/DotNet.Glob
- https://www.nuget.org/packages/DotNet.Glob/
我写了一个FileSelector类,它根据文件名选择文件。它还根据时间、大小和属性选择文件。如果您只想使用文件名globbing,那么您可以用"*"这样的形式来表示名称。txt";等等。如果你想要其他参数,那么你可以指定一个布尔逻辑语句,比如";name=*.xls和ctime<2009-01-01"-意味着在2009年1月1日之前创建的.xls文件。您也可以根据负片选择:";name!=*。xls";表示所有不是xls的文件。
看看吧。开源。自由许可。免费在其他地方使用。
基于之前的帖子,我构建了一个C#类:
using System;
using System.Text.RegularExpressions;
public class FileWildcard
{
Regex mRegex;
public FileWildcard(string wildcard)
{
string pattern = string.Format("^{0}$", Regex.Escape(wildcard)
.Replace(@"'*", ".*").Replace(@"'?", "."));
mRegex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
}
public bool IsMatch(string filenameToCompare)
{
return mRegex.IsMatch(filenameToCompare);
}
}
使用它会变成这样:
FileWildcard w = new FileWildcard("*.txt");
if (w.IsMatch("Doug.Txt"))
Console.WriteLine("We have a match");
匹配与系统不相同。IO.Directory.GetFiles()方法,所以不要将它们一起使用。
您可以使用C#。NET的LikeOperator。LikeString方法。这是VB的LIKE运算符的支持实现。它支持使用*、?、#、,[骗子]和[!骗子]。
您可以通过添加对Microsoft的引用来使用C#中的LikeString方法。VisualBasic.dll程序集,该程序集包含在的每个版本中。NET框架。然后调用LikeString方法,就像调用任何其他静态方法一样。NET方法:
using Microsoft.VisualBasic;
using Microsoft.VisualBasic.CompilerServices;
...
bool isMatch = LikeOperator.LikeString("I love .NET!", "I love *", CompareMethod.Text);
// isMatch should be true.
https://www.nuget.org/packages/Glob.cs
https://github.com/mganss/Glob.cs
GNU Glob。NET。
您可以在安装后去掉包引用,只需编译单个Glob.cs源文件。
由于它是GNU Glob的一个实现,一旦你找到另一个类似的实现,它就是跨平台和跨语言的!
我不知道。NET框架有glob匹配,但你不能用.*代替*吗?并使用正则表达式?
只是出于好奇,我浏览了一下微软。扩展。FileSystemGlobbing——它对很多库都有很大的依赖性——我决定为什么我不能尝试写类似的东西?
说起来容易做起来难,我很快注意到它毕竟不是一个微不足道的功能——例如,"*.txt"应该只直接匹配当前文件,而"**.txt"也应该获取子文件夹。
微软还测试了一些奇怪的匹配模式序列,比如"./*.txt"——我不确定谁真的需要"./"类型的字符串——因为它们在处理时会被删除。(https://github.com/aspnet/FileSystem/blob/dev/test/Microsoft.Extensions.FileSystemGlobbing.Tests/PatternMatchingTests.cs)
不管怎样,我已经编写了自己的函数——它将有两个副本——一个在svn中(我稍后可能会对它进行错误修复)——我也将在这里复制一个示例用于演示。我建议从svn链接复制粘贴。
SVN链接:
https://sourceforge.net/p/syncproj/code/HEAD/tree/SolutionProjectBuilder.cs#l800(如果跳转不正确,则搜索matchFiles功能)。
这里还有本地功能副本:
/// <summary>
/// Matches files from folder _dir using glob file pattern.
/// In glob file pattern matching * reflects to any file or folder name, ** refers to any path (including sub-folders).
/// ? refers to any character.
///
/// There exists also 3-rd party library for performing similar matching - 'Microsoft.Extensions.FileSystemGlobbing'
/// but it was dragging a lot of dependencies, I've decided to survive without it.
/// </summary>
/// <returns>List of files matches your selection</returns>
static public String[] matchFiles( String _dir, String filePattern )
{
if (filePattern.IndexOfAny(new char[] { '*', '?' }) == -1) // Speed up matching, if no asterisk / widlcard, then it can be simply file path.
{
String path = Path.Combine(_dir, filePattern);
if (File.Exists(path))
return new String[] { filePattern };
return new String[] { };
}
String dir = Path.GetFullPath(_dir); // Make it absolute, just so we can extract relative path'es later on.
String[] pattParts = filePattern.Replace("/", "''").Split('''');
List<String> scanDirs = new List<string>();
scanDirs.Add(dir);
//
// By default glob pattern matching specifies "*" to any file / folder name,
// which corresponds to any character except folder separator - in regex that's "[^'']*"
// glob matching also allow double astrisk "**" which also recurses into subfolders.
// We split here each part of match pattern and match it separately.
//
for (int iPatt = 0; iPatt < pattParts.Length; iPatt++)
{
bool bIsLast = iPatt == (pattParts.Length - 1);
bool bRecurse = false;
String regex1 = Regex.Escape(pattParts[iPatt]); // Escape special regex control characters ("*" => "'*", "." => "'.")
String pattern = Regex.Replace(regex1, @"'''*('''*)?", delegate (Match m)
{
if (m.ToString().Length == 4) // "**" => "'*'*" (escaped) - we need to recurse into sub-folders.
{
bRecurse = true;
return ".*";
}
else
return @"[^'']*";
}).Replace(@"'?", ".");
if (pattParts[iPatt] == "..") // Special kind of control, just to scan upper folder.
{
for (int i = 0; i < scanDirs.Count; i++)
scanDirs[i] = scanDirs[i] + "''..";
continue;
}
Regex re = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
int nScanItems = scanDirs.Count;
for (int i = 0; i < nScanItems; i++)
{
String[] items;
if (!bIsLast)
items = Directory.GetDirectories(scanDirs[i], "*", (bRecurse) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
else
items = Directory.GetFiles(scanDirs[i], "*", (bRecurse) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
foreach (String path in items)
{
String matchSubPath = path.Substring(scanDirs[i].Length + 1);
if (re.Match(matchSubPath).Success)
scanDirs.Add(path);
}
}
scanDirs.RemoveRange(0, nScanItems); // Remove items what we have just scanned.
} //for
// Make relative and return.
return scanDirs.Select( x => x.Substring(dir.Length + 1) ).ToArray();
} //matchFiles
如果你发现任何错误,我会尽快修复它们。
我写了一个解决方案。它不依赖于任何库,也不支持"!"或"[]"运算符。它支持以下搜索模式:
C: ''Logs''*.txt
C: ''日志''**''*P1''**''asd*.pdf
/// <summary>
/// Finds files for the given glob path. It supports ** * and ? operators. It does not support !, [] or ![] operators
/// </summary>
/// <param name="path">the path</param>
/// <returns>The files that match de glob</returns>
private ICollection<FileInfo> FindFiles(string path)
{
List<FileInfo> result = new List<FileInfo>();
//The name of the file can be any but the following chars '<','>',':','/',''','|','?','*','"'
const string folderNameCharRegExp = @"[^'<'>:/'''|'?'*" + "'"]";
const string folderNameRegExp = folderNameCharRegExp + "+";
//We obtain the file pattern
string filePattern = Path.GetFileName(path);
List<string> pathTokens = new List<string>(Path.GetDirectoryName(path).Split('''', '/'));
//We obtain the root path from where the rest of files will obtained
string rootPath = null;
bool containsWildcardsInDirectories = false;
for (int i = 0; i < pathTokens.Count; i++)
{
if (!pathTokens[i].Contains("*")
&& !pathTokens[i].Contains("?"))
{
if (rootPath != null)
rootPath += "''" + pathTokens[i];
else
rootPath = pathTokens[i];
pathTokens.RemoveAt(0);
i--;
}
else
{
containsWildcardsInDirectories = true;
break;
}
}
if (Directory.Exists(rootPath))
{
//We build the regular expression that the folders should match
string regularExpression = rootPath.Replace("''", "''''").Replace(":", "'':").Replace(" ", "''s");
foreach (string pathToken in pathTokens)
{
if (pathToken == "**")
{
regularExpression += string.Format(CultureInfo.InvariantCulture, @"(''{0})*", folderNameRegExp);
}
else
{
regularExpression += @"''" + pathToken.Replace("*", folderNameCharRegExp + "*").Replace(" ", "''s").Replace("?", folderNameCharRegExp);
}
}
Regex globRegEx = new Regex(regularExpression, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
string[] directories = Directory.GetDirectories(rootPath, "*", containsWildcardsInDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
foreach (string directory in directories)
{
if (globRegEx.Matches(directory).Count > 0)
{
DirectoryInfo directoryInfo = new DirectoryInfo(directory);
result.AddRange(directoryInfo.GetFiles(filePattern));
}
}
}
return result;
}
不幸的是,接受的答案无法正确处理转义输入,因为字符串.Replace("'*", ".*")
无法区分"*"以及"''*"-它将很高兴地取代"*"在这两个字符串中,导致不正确的结果。
相反,可以使用一个基本的标记化器将glob路径转换为regex模式,然后可以使用Regex.Match
将其与文件名进行匹配。这是一个更加稳健和灵活的解决方案。
这里有一个方法。它处理?
、*
和**
,并用捕获组包围这些glob中的每一个,因此可以在Regex匹配后检查每个glob的值。
static string GlobbedPathToRegex(ReadOnlySpan<char> pattern, ReadOnlySpan<char> dirSeparatorChars)
{
StringBuilder builder = new StringBuilder();
builder.Append('^');
ReadOnlySpan<char> remainder = pattern;
while (remainder.Length > 0)
{
int specialCharIndex = remainder.IndexOfAny('*', '?');
if (specialCharIndex >= 0)
{
ReadOnlySpan<char> segment = remainder.Slice(0, specialCharIndex);
if (segment.Length > 0)
{
string escapedSegment = Regex.Escape(segment.ToString());
builder.Append(escapedSegment);
}
char currentCharacter = remainder[specialCharIndex];
char nextCharacter = specialCharIndex < remainder.Length - 1 ? remainder[specialCharIndex + 1] : ''0';
switch (currentCharacter)
{
case '*':
if (nextCharacter == '*')
{
// We have a ** glob expression
// Match any character, 0 or more times.
builder.Append("(.*)");
// Skip over **
remainder = remainder.Slice(specialCharIndex + 2);
}
else
{
// We have a * glob expression
// Match any character that isn't a dirSeparatorChar, 0 or more times.
if(dirSeparatorChars.Length > 0) {
builder.Append($"([^{Regex.Escape(dirSeparatorChars.ToString())}]*)");
}
else {
builder.Append("(.*)");
}
// Skip over *
remainder = remainder.Slice(specialCharIndex + 1);
}
break;
case '?':
builder.Append("(.)"); // Regex equivalent of ?
// Skip over ?
remainder = remainder.Slice(specialCharIndex + 1);
break;
}
}
else
{
// No more special characters, append the rest of the string
string escapedSegment = Regex.Escape(remainder.ToString());
builder.Append(escapedSegment);
remainder = ReadOnlySpan<char>.Empty;
}
}
builder.Append('$');
return builder.ToString();
}
使用它:
string testGlobPathInput = "/Hello/Test/Blah/**/test*123.fil?";
string globPathRegex = GlobbedPathToRegex(testGlobPathInput, "/"); // Could use "''/" directory separator chars on Windows
Console.WriteLine($"Globbed path: {testGlobPathInput}");
Console.WriteLine($"Regex conversion: {globPathRegex}");
string testPath = "/Hello/Test/Blah/All/Hail/The/Hypnotoad/test_somestuff_123.file";
Console.WriteLine($"Test Path: {testPath}");
var regexGlobPathMatch = Regex.Match(testPath, globPathRegex);
Console.WriteLine($"Match: {regexGlobPathMatch.Success}");
for(int i = 0; i < regexGlobPathMatch.Groups.Count; i++) {
Console.WriteLine($"Group [{i}]: {regexGlobPathMatch.Groups[i]}");
}
输出:
Globbed path: /Hello/Test/Blah/**/test*123.fil?
Regex conversion: ^/Hello/Test/Blah/(.*)/test([^/]*)123'.fil(.)$
Test Path: /Hello/Test/Blah/All/Hail/The/Hypnotoad/test_somestuff_123.file
Match: True
Group [0]: /Hello/Test/Blah/All/Hail/The/Hypnotoad/test_somestuff_123.file
Group [1]: All/Hail/The/Hypnotoad
Group [2]: _somestuff_
Group [3]: e
我在这里创建了一个要点,作为这种方法的规范版本:
https://gist.github.com/crozone/9a10156a37c978e098e43d800c6141ad