56

.NET 中是否有内置机制来匹配正则表达式以外的模式?我想使用 UNIX 样式(glob)通配符(* = 任意数量的任意字符)进行匹配。

我想将其用于面向最终用户的控件。我担心允许所有 RegEx 功能会非常混乱。

4

15 回答 15

71

我喜欢我的代码更语义化,所以我写了这个扩展方法:

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"))
{
   ....
}

只是糖让你的代码更易读:-)

于 2010-11-10T15:56:14.477 回答
41

只是为了完整性。自 2016 年以来,dotnet core有一个名为Microsoft.Extensions.FileSystemGlobbing支持高级全局路径的新 nuget 包。(Nuget 包

一些示例可能是,搜索通配符嵌套的文件夹结构和文件,这在 Web 开发场景中非常常见。

  • wwwroot/app/**/*.module.js
  • wwwroot/app/**/*.js

这与使用哪些.gitignore文件来确定要从源代码管理中排除哪些文件有点类似。

于 2016-06-03T09:56:24.443 回答
40

我为您找到了实际代码:

Regex.Escape( wildcardExpression ).Replace( @"\*", ".*" ).Replace( @"\?", "." );
于 2008-10-10T06:04:16.467 回答
11

列表方法的 2 和 3 参数变体像GetFiles()EnumerateDirectories()将搜索字符串作为支持文件名通配符的第二个参数,同时使用*?

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变体也支持这一点。

于 2010-08-25T00:35:35.047 回答
5

如果你使用 VB.Net,你可以使用 Like 语句,它有类似 Glob 的语法。

http://www.getdotnetcode.com/gdncstore/free/Articles/Intoduction%20to%20the%20VB%20NET%20Like%20Operator.htm

于 2008-10-09T19:54:23.810 回答
4

我写了一个FileSelector类,它根据文件名选择文件。它还根据时间、大小和属性选择文件。如果您只想要文件名通配符,那么您可以用“*.txt”等形式表达名称。如果您需要其他参数,那么您可以指定一个布尔逻辑语句,例如“name = *.xls and ctime < 2009-01-01” - 暗示在 2009 年 1 月 1 日之前创建的 .xls 文件。您还可以根据否定选择: "name != *.xls" 表示所有不是 xls 的文件。

看看这个。开源。自由许可证。在其他地方免费使用。

于 2009-03-05T06:54:13.183 回答
4

我为 .NETStandard 编写了一个包含测试和基准的全局库。我的目标是为 .NET 生成一个库,它具有最小的依赖关系,不使用 Regex,并且优于 Regex。

你可以在这里找到它:

于 2018-09-11T18:12:43.353 回答
3

如果你想避免使用正则表达式,这是一个基本的 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"));
于 2011-11-11T13:00:41.920 回答
2

根据之前的帖子,我整理了一个 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");

匹配与 System.IO.Directory.GetFiles() 方法不同,所以不要一起使用。

于 2010-08-04T17:49:21.183 回答
2

在 C# 中,您可以使用 .NET 的LikeOperator.LikeString方法。这是 VB 的LIKE 运算符的支持实现。它支持使用 *、?、#、[charlist] 和 [!charlist] 的模式。

您可以通过添加对 Microsoft.VisualBasic.dll 程序集的引用来使用 C# 中的 LikeString 方法,该程序集包含在每个版本的 .NET Framework 中。然后像任何其他静态 .NET 方法一样调用 LikeString 方法:

using Microsoft.VisualBasic;
using Microsoft.VisualBasic.CompilerServices;
...
bool isMatch = LikeOperator.LikeString("I love .NET!", "I love *", CompareMethod.Text);
// isMatch should be true.
于 2016-08-18T18:06:21.500 回答
2

https://www.nuget.org/packages/Glob.cs

https://github.com/mgans/Glob.cs

.NET 的 GNU Glob。

您可以在安装后摆脱包引用,只需编译单个 Glob.cs 源文件。

由于它是 GNU Glob 的实现,因此一旦您找到另一个类似的实现,它就可以跨平台和跨语言享受!

于 2017-08-06T13:18:25.107 回答
1

我不知道 .NET 框架是否有 glob 匹配,但你不能用 .* 替换 * 吗?并使用正则表达式?

于 2008-10-09T19:52:55.140 回答
0

只是出于好奇,我浏览了 Microsoft.Extensions.FileSystemGlobbing - 它拖累了对很多库的巨大依赖 - 我已经决定为什么我不能尝试编写类似的东西?

嗯 - 说起来容易做起来难,我很快注意到它毕竟不是那么微不足道的功能 - 例如“*.txt”应该只匹配当前文件,而“**.txt”也应该收获子文件夹。

Microsoft 还测试了一些奇怪的匹配模式序列,例如“./*.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

如果你发现任何错误,我会毕业来修复它们。

于 2017-03-05T13:45:31.693 回答
0

我写了一个解决方案。它不依赖于任何库,也不支持“!” 或“[]”运算符。它支持以下搜索模式:

C:\Logs\*.txt

C:\Logs\**\*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;
    }
于 2017-04-04T14:16:41.467 回答
0

不幸的是,接受的答案将无法正确处理转义输入,因为字符串.Replace("\*", ".*")无法区分“*”和“\*” - 它会很乐意替换这两个字符串中的“*”,从而导致不正确的结果。

相反,可以使用基本标记器将 glob 路径转换为正则表达式模式,然后可以使用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

于 2021-08-26T00:58:41.047 回答