如何确定文件是否匹配文件掩码?

31
我需要决定文件名是否符合文件掩码。文件掩码可以包含 * 或 ? 字符。有没有简单的解决方案?
bool bFits = Fits("myfile.txt", "my*.txt");

private bool Fits(string sFileName, string sFileMask)
    {
        ??? anything simple here ???
    }
13个回答

31

我很感激找到Joel的答案,也省了我不少时间!但是,我不得不做一些更改使得这个方法符合大多数用户的预期:

  • 我去掉了第一个参数前面的“this”关键字。在这里它没有作用(虽然如果该方法旨在成为扩展方法,则它可能有用,此时需要将其放在静态类中并且本身也是静态方法)。
  • 我将正则表达式设置为不区分大小写,以匹配标准的Windows通配符行为(因此,“c*.*”和“C*.*”都返回相同的结果)。
  • 我添加了正则表达式的起始和结束锚点,再次匹配标准的Windows通配符行为(例如,“stuff.txt”将与“stuff *”或“s *”或“s * .*”匹配,但只有“s”不能匹配“stuff.txt”)。

private bool FitsMask(string fileName, string fileMask)
{
    Regex mask = new Regex(
        '^' + 
        fileMask
            .Replace(".", "[.]")
            .Replace("*", ".*")
            .Replace("?", ".")
        + '$',
        RegexOptions.IgnoreCase);
    return mask.IsMatch(fileName);
}

2009.11.04 更新:匹配多个掩码

为了更灵活,这里提供了一个兼容原始方法的插件。这个版本允许您传递多个掩码(因此第二个参数名 fileMasks 是复数形式),这些掩码可以用换行符、逗号、竖线或空格分隔。我想让用户在ListBox中放置尽可能多的选择,然后选择与任何之一匹配的文件。请注意,某些控件(如ListBox)使用CR-LF作为换行符,而其他控件(例如RichTextBox)只使用LF,这就是为什么在Split列表中同时出现"\r\n"和"\n"的原因。

private bool FitsOneOfMultipleMasks(string fileName, string fileMasks)
{
    return fileMasks
        .Split(new string[] {"\r\n", "\n", ",", "|", " "},
            StringSplitOptions.RemoveEmptyEntries)
        .Any(fileMask => FitsMask(fileName, fileMask));
}

2009.11.17更新:更优雅地处理文件掩码输入

以前版本的FitsMask(为了比较,我已经保留)工作得还不错,但由于我们将其视为正则表达式,在输入无效的正则表达式时会抛出异常。解决方案是,实际上我们希望将输入文件掩码中的任何正则表达式元字符都视为字面值,而不是元字符。但我们仍然需要特别处理句点、星号和问号。因此,这个改进版的FitsMask安全地将这三个字符放到一边,将所有剩余的元字符转换为字面值,然后以它们“被正则表达式化”的形式将这三个有趣的字符放回去。

另一个小改进是允许按照标准Windows行为进行大小写不敏感的匹配。

private bool FitsMask(string fileName, string fileMask)
{
    string pattern =
         '^' + 
         Regex.Escape(fileMask.Replace(".", "__DOT__")
                         .Replace("*", "__STAR__")
                         .Replace("?", "__QM__"))
             .Replace("__DOT__", "[.]")
             .Replace("__STAR__", ".*")
             .Replace("__QM__", ".")
         + '$';
    return new Regex(pattern, RegexOptions.IgnoreCase).IsMatch(fileName);
}

2010.09.30 更新:在某个时刻,激情突然降临...

我一直没有及时更新,但这些参考资料可能会对那些已经看到这里的读者有所帮助:

  • 我将FitsMask方法嵌入到一个WinForms用户控件中,恰当地称为FileMask--请看API这里
  • 接着,我写了一篇文章,在Simple-Talk.com上发表,介绍如何使用LINQ Lambda表达式设计可定制的通用组件,其中涉及到了FileMask控件--文章标题为Using LINQ Lambda Expressions to Design Customizable Generic Components. (虽然该方法本身并不使用LINQ,但是FileMask用户控件确实使用了,因此文章标题就是这样取的。)

1
你的代码仍然存在问题。所有这些字符都可以在文件名中使用,但在正则表达式中具有特殊含义:^$+=!{,}。你需要像转义点字符一样转义它们。 - Artemix
1
那些 __DOT__ / __STAR__ / __QM__ 的东西仍然会在任何包含这些特殊替换字符串的文件名上出现问题。首先进行转义,然后再替换转义后的字符串是唯一的好解决方案。特别是因为(Windows)文件名不能包含 \,所以转义的 \ 字符不是问题。 - Nyerguds
3
@Nyderguds提出了另一种解决方案,我建议大家往下滚动查找。 - Jason Glover
这对我来说是最好的答案。被接受的答案不完整。'^' ... '$' 缺失了。 - Mathias Müller

22

试一下这个:

private bool FitsMask(string sFileName, string sFileMask)
{
    Regex mask = new Regex(sFileMask.Replace(".", "[.]").Replace("*", ".*").Replace("?", "."));
    return mask.IsMatch(sFileName);
}

3
如果您在查看窗口,则不正确。例如,给定掩码“*.asp”,它将匹配“foo.asp”、“foo.aspx”和“foo.aspxx”,但是给定掩码“*.aspx”,它只会匹配“foo.aspx”。对于三个字符的扩展名有特殊规则。 - Greg Beech
这似乎不是一个很好的解决方案。 给定掩码“jpg”,pic.jpg 可以通过。 我期望"*.jpg"匹配。 - Mike Cole
1
现在,让我们尝试使用掩码*$.com来匹配文件file$name.com。正则表达式将是.$.*[.]com,它将无法匹配文件名。因此,您需要转义所有具有正则表达式中特殊含义的有效字符:^$+=!{,}。 - Artemix
你可以通过使用Regex.Escape()直接开始。 - Nyerguds
3
@Nyderguds提出了一个替代方案,我建议人们向下滚动以找到它。 - Jason Glover
显示剩余2条评论

17

许多人不知道,但.NET包括一个名为“PatternMatcher”的内部类(在“System.IO”命名空间下)。

这个静态类只包含一个方法:public static bool StrictMatchPattern(string expression, string name)

每当.NET需要使用通配符(FileSystemWatcher、GetFiles()等)比较文件时,就会使用这个方法。

我使用反编译工具公开了这里的代码。虽然我没有深入了解它的工作原理,但它运行得很好,

所以这是代码,供那些不希望使用低效的RegEx方法的人使用:

public static class PatternMatcher
{
    // Fields
    private const char ANSI_DOS_QM = '<';
    private const char ANSI_DOS_STAR = '>';
    private const char DOS_DOT = '"';
    private const int MATCHES_ARRAY_SIZE = 16;

    // Methods
    public static bool StrictMatchPattern(string expression, string name)
    {
        expression = expression.ToLowerInvariant();
        name = name.ToLowerInvariant();
        int num9;
        char ch = '\0';
        char ch2 = '\0';
        int[] sourceArray = new int[16];
        int[] numArray2 = new int[16];
        bool flag = false;
        if (((name == null) || (name.Length == 0)) || ((expression == null) || (expression.Length == 0)))
        {
            return false;
        }
        if (expression.Equals("*") || expression.Equals("*.*"))
        {
            return true;
        }
        if ((expression[0] == '*') && (expression.IndexOf('*', 1) == -1))
        {
            int length = expression.Length - 1;
            if ((name.Length >= length) && (string.Compare(expression, 1, name, name.Length - length, length, StringComparison.OrdinalIgnoreCase) == 0))
            {
                return true;
            }
        }
        sourceArray[0] = 0;
        int num7 = 1;
        int num = 0;
        int num8 = expression.Length * 2;
        while (!flag)
        {
            int num3;
            if (num < name.Length)
            {
                ch = name[num];
                num3 = 1;
                num++;
            }
            else
            {
                flag = true;
                if (sourceArray[num7 - 1] == num8)
                {
                    break;
                }
            }
            int index = 0;
            int num5 = 0;
            int num6 = 0;
            while (index < num7)
            {
                int num2 = (sourceArray[index++] + 1) / 2;
                num3 = 0;
            Label_00F2:
                if (num2 != expression.Length)
                {
                    num2 += num3;
                    num9 = num2 * 2;
                    if (num2 == expression.Length)
                    {
                        numArray2[num5++] = num8;
                    }
                    else
                    {
                        ch2 = expression[num2];
                        num3 = 1;
                        if (num5 >= 14)
                        {
                            int num11 = numArray2.Length * 2;
                            int[] destinationArray = new int[num11];
                            Array.Copy(numArray2, destinationArray, numArray2.Length);
                            numArray2 = destinationArray;
                            destinationArray = new int[num11];
                            Array.Copy(sourceArray, destinationArray, sourceArray.Length);
                            sourceArray = destinationArray;
                        }
                        if (ch2 == '*')
                        {
                            numArray2[num5++] = num9;
                            numArray2[num5++] = num9 + 1;
                            goto Label_00F2;
                        }
                        if (ch2 == '>')
                        {
                            bool flag2 = false;
                            if (!flag && (ch == '.'))
                            {
                                int num13 = name.Length;
                                for (int i = num; i < num13; i++)
                                {
                                    char ch3 = name[i];
                                    num3 = 1;
                                    if (ch3 == '.')
                                    {
                                        flag2 = true;
                                        break;
                                    }
                                }
                            }
                            if ((flag || (ch != '.')) || flag2)
                            {
                                numArray2[num5++] = num9;
                                numArray2[num5++] = num9 + 1;
                            }
                            else
                            {
                                numArray2[num5++] = num9 + 1;
                            }
                            goto Label_00F2;
                        }
                        num9 += num3 * 2;
                        switch (ch2)
                        {
                            case '<':
                                if (flag || (ch == '.'))
                                {
                                    goto Label_00F2;
                                }
                                numArray2[num5++] = num9;
                                goto Label_028D;

                            case '"':
                                if (flag)
                                {
                                    goto Label_00F2;
                                }
                                if (ch == '.')
                                {
                                    numArray2[num5++] = num9;
                                    goto Label_028D;
                                }
                                break;
                        }
                        if (!flag)
                        {
                            if (ch2 == '?')
                            {
                                numArray2[num5++] = num9;
                            }
                            else if (ch2 == ch)
                            {
                                numArray2[num5++] = num9;
                            }
                        }
                    }
                }
            Label_028D:
                if ((index < num7) && (num6 < num5))
                {
                    while (num6 < num5)
                    {
                        int num14 = sourceArray.Length;
                        while ((index < num14) && (sourceArray[index] < numArray2[num6]))
                        {
                            index++;
                        }
                        num6++;
                    }
                }
            }
            if (num5 == 0)
            {
                return false;
            }
            int[] numArray4 = sourceArray;
            sourceArray = numArray2;
            numArray2 = numArray4;
            num7 = num5;
        }
        num9 = sourceArray[num7 - 1];
        return (num9 == num8);
    }
}

2
那真是...令人困惑地愚蠢。哇,他们为什么不暴露那个呢?X_x - Nyerguds
4
我同意Nyeguds的观点,它本应该是System.IO.Path类的一部分! - dmihailescu
实际的源代码已发布在https://blogs.msdn.microsoft.com/jeremykuhne/2017/06/04/wildcards-in-windows/。 - Andrew Rondeau
3
现在我们有源代码:https://referencesource.microsoft.com/#System/services/io/system/io/PatternMatcher.cs - JDC

14

这些答案似乎都不太行,而msorens的答案过于复杂。这个应该就可以解决问题了:

public static Boolean MatchesMask(string fileName, string fileMask)
{
    String convertedMask = "^" + Regex.Escape(fileMask).Replace("\\*", ".*").Replace("\\?", ".") + "$";
    Regex regexMask = new Regex(convertedMask, RegexOptions.IgnoreCase);
    return regexMask.IsMatch(fileName);
}

这样可以确保掩码中可能存在的正则表达式字符被转义,取代\*和\?,并用^和$围绕起来标记边界。

当然,在大多数情况下,更有用的是将其制作成一个FileMaskToRegex工具函数,该函数返回Regex对象,因此您只需要获取它一次,然后可以在其中进行循环,检查文件列表中的所有字符串。

public static Regex FileMaskToRegex(string fileMask)
{
    String convertedMask = "^" + Regex.Escape(fileMask).Replace("\\*", ".*").Replace("\\?", ".") + "$";
    return new Regex(convertedMask, RegexOptions.IgnoreCase);
}

4

从Windows 7使用P/Invoke(无260字符计数限制):

// UNICODE_STRING for Rtl... method
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UNICODE_STRING
{
    public ushort Length;
    public ushort MaximumLength;
    [MarshalAs(UnmanagedType.LPWStr)]
    string Buffer;

    public UNICODE_STRING(string buffer)
    {
        if (buffer == null)
            Length = MaximumLength = 0;
        else
            Length = MaximumLength = unchecked((ushort)(buffer.Length * 2));
        Buffer = buffer;
    }
}

// RtlIsNameInExpression method from NtDll.dll system library
public static class NtDll
{
    [DllImport("NtDll.dll", CharSet=CharSet.Unicode, ExactSpelling=true)]
    [return: MarshalAs(UnmanagedType.U1)]
    public extern static bool RtlIsNameInExpression(
        ref UNICODE_STRING Expression,
        ref UNICODE_STRING Name,
        [MarshalAs(UnmanagedType.U1)]
        bool IgnoreCase,
        IntPtr Zero
        );
}

public bool MatchMask(string mask, string fileName)
{
    // Expression must be uppercase for IgnoreCase == true (see MSDN for RtlIsNameInExpression)
    UNICODE_STRING expr = new UNICODE_STRING(mask.ToUpper());
    UNICODE_STRING name = new UNICODE_STRING(fileName);

    if (NtDll.RtlIsNameInExpression(ref expr, ref name, true, IntPtr.Zero))
    {
        // MATCHES !!!
    }
}

2
那么,为什么不让它实际“返回”布尔结果,而不是添加那个“// MATCHES !!!”注释呢? - Nyerguds

4

使用 WildCardPattern 类,该类位于 System.Management.Automation 中,并且可通过 NuGet 包 或 Windows PowerShell SDK 获取。

WildcardPattern pattern = new WildcardPattern("my*.txt");
bool fits = pattern.IsMatch("myfile.txt");

3

之前提出的函数中最快的版本:

    public static bool FitsMasks(string filePath, params string[] fileMasks)
            // or
    public static Regex FileMasksToRegex(params string[] fileMasks)
    {
        if (!_maskRegexes.ContainsKey(fileMasks))
        {
            StringBuilder sb = new StringBuilder("^");
            bool first = true;
            foreach (string fileMask in fileMasks)
            {
                if(first) first =false; else sb.Append("|");
                sb.Append('(');
                foreach (char c in fileMask)
                {
                    switch (c)
                    {
                        case '*': sb.Append(@".*"); break;
                        case '?': sb.Append(@"."); break;
                        default:
                                sb.Append(Regex.Escape(c.ToString()));
                            break;
                    }
                }
                sb.Append(')');
            }
            sb.Append("$");
            _maskRegexes[fileMasks] = new Regex(sb.ToString(), RegexOptions.IgnoreCase);
        }
        return _maskRegexes[fileMasks].IsMatch(filePath);
                    // or
        return _maskRegexes[fileMasks];
    }
    static readonly Dictionary<string[], Regex> _maskRegexes = new Dictionary<string[], Regex>(/*unordered string[] comparer*/);

注意:

  1. 重复使用正则表达式对象。
  2. 使用StringBuilder来优化正则表达式的创建(多次调用.Replace()会很慢)。
  3. 多个掩码,结合OR操作符。
  4. 另一个版本返回正则表达式。

实际上,我可以想象这种函数通常在整个列表上使用,因此在过滤循环之前先创建正则表达式,然后通过foreach遍历列表来使用过滤器,可以解决每次重建正则表达式的低效率问题。 - Nyerguds
@Nyerguds 同意。然而,上述代码可以被适应于即使对于循环遍历多个文件也能产生Regex对象,即使只有一点性能提升。顺便说一下,一个简单的改进可以允许传入多个掩码进行“或”比较。 - Mr. TA
没错。我会选择一个Regex FileMaskToRegex(String fileMask)函数,因为它更通用。另外,关于你的代码还有一个小注释:过滤“.”是完全不必要的,因为你特定情况下的代码与默认情况下的代码完全相同,即转义它。你还忘记了表示表达式开头的“^”符号。 - Nyerguds
嘿。如果你仍然将掩码存储在那个字典中,返回Regex对象并不特别有用。我只是想说,返回正则表达式通常是字典的一个很好的替代方案。还是很不错的 :) - Nyerguds
相关的是,你可以通过对文件掩码数组进行排序来进一步优化,这样你就可以确保不会为相同但排序不同的掩码列表创建不同的正则表达式。当然,这种优化是否实际有用完全取决于具体情况。 - Nyerguds
请注意 /*unordered string[] comparer*/ 的注释 - 我考虑过这一点,因此使用了 unordered。关于字典,即使用户多次请求“搜索”,仍然可能有益,每次都需要检索正则表达式。该字典通过存储特定掩码集的先前创建的正则表达式来节省时间。顺便说一下,字典的存储和查找可能比重建正则表达式更昂贵 - 由编码人员在其特定情况下做出决定。 - Mr. TA

2
如果 PowerShell 可用,则直接支持 通配符类型匹配(以及正则表达式)。
WildcardPattern pat = new WildcardPattern("a*.b*");
if (pat.IsMatch(filename)) { ... }

2

我不想复制源代码,就像@frankhommers一样,我想出了一个基于反射的解决方案。

请注意,在参考源代码中我发现了关于在名称参数中使用通配符的代码注释。

最初的回答:

    public static class PatternMatcher
    {
        static MethodInfo strictMatchPatternMethod;
        static PatternMatcher()
        {
            var typeName = "System.IO.PatternMatcher";
            var methodName = "StrictMatchPattern";
            var assembly = typeof(Uri).Assembly;
            var type = assembly.GetType(typeName, true);
            strictMatchPatternMethod = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public) ?? throw new MissingMethodException($"{typeName}.{methodName} not found");
        }

        /// <summary>
        /// Tells whether a given name matches the expression given with a strict (i.e. UNIX like) semantics.
        /// </summary>
        /// <param name="expression">Supplies the input expression to check against</param>
        /// <param name="name">Supplies the input name to check for.</param>
        /// <returns></returns>
        public static bool StrictMatchPattern(string expression, string name)
        {
            // https://referencesource.microsoft.com/#system/services/io/system/io/PatternMatcher.cs
            // If this class is ever exposed for generic use,
            // we need to make sure that name doesn't contain wildcards. Currently 
            // the only component that calls this method is FileSystemWatcher and
            // it will never pass a name that contains a wildcard.
            if (name.Contains('*')) throw new FormatException("Wildcard not allowed");
            return (bool)strictMatchPatternMethod.Invoke(null, new object[] { expression, name });
        }
    }

1
对于 .NET Core,微软的做法是什么
        private bool MatchPattern(ReadOnlySpan<char> relativePath)
        {
            ReadOnlySpan<char> name = IO.Path.GetFileName(relativePath);
            if (name.Length == 0)
                return false;

            if (Filters.Count == 0)
                return true;

            foreach (string filter in Filters)
            {
                if (FileSystemName.MatchesSimpleExpression(filter, name, ignoreCase: !PathInternal.IsCaseSensitive))
                    return true;
            }

            return false;
        }

微软本身似乎为.NET 4.6所做的方式在github中有记录:

    private bool MatchPattern(string relativePath) {            
        string name = System.IO.Path.GetFileName(relativePath);            
        if (name != null)
            return PatternMatcher.StrictMatchPattern(filter.ToUpper(CultureInfo.InvariantCulture), name.ToUpper(CultureInfo.InvariantCulture));
        else
            return false;                
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接