Need to perform Wildcard (*,?, etc) search on a string using Regex

124,079

Solution 1

From http://www.codeproject.com/KB/recipes/wildcardtoregex.aspx:

public static string WildcardToRegex(string pattern)
{
    return "^" + Regex.Escape(pattern)
                      .Replace(@"\*", ".*")
                      .Replace(@"\?", ".")
               + "$";
}

So something like foo*.xls? will get transformed to ^foo.*\.xls.$.

Solution 2

You can do a simple wildcard mach without RegEx using a Visual Basic function called LikeString.

using Microsoft.VisualBasic;
using Microsoft.VisualBasic.CompilerServices;

if (Operators.LikeString("This is just a test", "*just*", CompareMethod.Text))
{
  Console.WriteLine("This matched!");
}

If you use CompareMethod.Text it will compare case-insensitive. For case-sensitive comparison, you can use CompareMethod.Binary.

More info here: http://www.henrikbrinch.dk/Blog/2012/02/14/Wildcard-matching-in-C

MSDN: http://msdn.microsoft.com/en-us/library/microsoft.visualbasic.compilerservices.operators.likestring%28v=vs.100%29.ASPX

Solution 3

The correct regular expression formulation of the glob expression d* is ^d, which means match anything that starts with d.

    string input = "Message";
    string pattern = @"^d";
    Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);

(The @ quoting is not necessary in this case, but good practice since many regexes use backslash escapes that need to be left alone, and it also indicates to the reader that this string is special).

Solution 4

Windows and *nux treat wildcards differently. *, ? and . are processed in a very complex way by Windows, one's presence or position would change another's meaning. While *nux keeps it simple, all it does is just one simple pattern match. Besides that, Windows matches ? for 0 or 1 chars, Linux matches it for exactly 1 chars.

I didn't find authoritative documents on this matter, here is just my conclusion based on days of tests on Windows 8/XP (command line, dir command to be specific, and the Directory.GetFiles method uses the same rules too) and Ubuntu Server 12.04.1 (ls command). I made tens of common and uncommon cases work, although there'are many failed cases too.

The current answer by Gabe, works like *nux. If you also want a Windows style one, and are willing to accept the imperfection, then here it is:

    /// <summary>
    /// <para>Tests if a file name matches the given wildcard pattern, uses the same rule as shell commands.</para>
    /// </summary>
    /// <param name="fileName">The file name to test, without folder.</param>
    /// <param name="pattern">A wildcard pattern which can use char * to match any amount of characters; or char ? to match one character.</param>
    /// <param name="unixStyle">If true, use the *nix style wildcard rules; otherwise use windows style rules.</param>
    /// <returns>true if the file name matches the pattern, false otherwise.</returns>
    public static bool MatchesWildcard(this string fileName, string pattern, bool unixStyle)
    {
        if (fileName == null)
            throw new ArgumentNullException("fileName");

        if (pattern == null)
            throw new ArgumentNullException("pattern");

        if (unixStyle)
            return WildcardMatchesUnixStyle(pattern, fileName);

        return WildcardMatchesWindowsStyle(fileName, pattern);
    }

    private static bool WildcardMatchesWindowsStyle(string fileName, string pattern)
    {
        var dotdot = pattern.IndexOf("..", StringComparison.Ordinal);
        if (dotdot >= 0)
        {
            for (var i = dotdot; i < pattern.Length; i++)
                if (pattern[i] != '.')
                    return false;
        }

        var normalized = Regex.Replace(pattern, @"\.+$", "");
        var endsWithDot = normalized.Length != pattern.Length;

        var endWeight = 0;
        if (endsWithDot)
        {
            var lastNonWildcard = normalized.Length - 1;
            for (; lastNonWildcard >= 0; lastNonWildcard--)
            {
                var c = normalized[lastNonWildcard];
                if (c == '*')
                    endWeight += short.MaxValue;
                else if (c == '?')
                    endWeight += 1;
                else
                    break;
            }

            if (endWeight > 0)
                normalized = normalized.Substring(0, lastNonWildcard + 1);
        }

        var endsWithWildcardDot = endWeight > 0;
        var endsWithDotWildcardDot = endsWithWildcardDot && normalized.EndsWith(".");
        if (endsWithDotWildcardDot)
            normalized = normalized.Substring(0, normalized.Length - 1);

        normalized = Regex.Replace(normalized, @"(?!^)(\.\*)+$", @".*");

        var escaped = Regex.Escape(normalized);
        string head, tail;

        if (endsWithDotWildcardDot)
        {
            head = "^" + escaped;
            tail = @"(\.[^.]{0," + endWeight + "})?$";
        }
        else if (endsWithWildcardDot)
        {
            head = "^" + escaped;
            tail = "[^.]{0," + endWeight + "}$";
        }
        else
        {
            head = "^" + escaped;
            tail = "$";
        }

        if (head.EndsWith(@"\.\*") && head.Length > 5)
        {
            head = head.Substring(0, head.Length - 4);
            tail = @"(\..*)?" + tail;
        }

        var regex = head.Replace(@"\*", ".*").Replace(@"\?", "[^.]?") + tail;
        return Regex.IsMatch(fileName, regex, RegexOptions.IgnoreCase);
    }

    private static bool WildcardMatchesUnixStyle(string pattern, string text)
    {
        var regex = "^" + Regex.Escape(pattern)
                               .Replace("\\*", ".*")
                               .Replace("\\?", ".")
                    + "$";

        return Regex.IsMatch(text, regex);
    }

There's a funny thing, even the Windows API PathMatchSpec does not agree with FindFirstFile. Just try a1*., FindFirstFile says it matches a1, PathMatchSpec says not.

Solution 5

d* means that it should match zero or more "d" characters. So any string is a valid match. Try d+ instead!

In order to have support for wildcard patterns I would replace the wildcards with the RegEx equivalents. Like * becomes .* and ? becomes .?. Then your expression above becomes d.*

Share:
124,079
Admin
Author by

Admin

Updated on January 22, 2021

Comments

  • Admin
    Admin over 3 years

    I need to perform Wildcard (*, ?, etc.) search on a string. This is what I have done:

    string input = "Message";
    string pattern = "d*";
    Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
    
    if (regex.IsMatch(input))
    {
        MessageBox.Show("Found");
    }
    else
    {
        MessageBox.Show("Not Found");
    }
    

    With the above code "Found" block is hitting but actually it should not!

    If my pattern is "e*" then only "Found" should hit.

    My understanding or requirement is d* search should find the text containing "d" followed by any characters.

    Should I change my pattern as "d.*" and "e.*"? Is there any support in .NET for Wild Card which internally does it while using Regex class?

  • Mark Lakata
    Mark Lakata almost 11 years
    Why was this downvoted? This is the correct solution to the OP's example. ^d.* does the same thing as ^d but without superfluous characters.
  • deerchao
    deerchao almost 11 years
    For those who really want to have the exact behavior of Windows, I can only give you a hint: FsRtlIsNameInExpression and FindFirstFile.
  • Marcel Popescu
    Marcel Popescu almost 11 years
    +1 for the research, especially the discovery that the two APIs disagree
  • Gabe
    Gabe almost 11 years
    My answer is complete. The Regex.Escape does exactly what you suggested.
  • Camarada
    Camarada over 10 years
    Sorry, Dude. I've made mistake
  • Disillusioned
    Disillusioned over 10 years
    +1 for being the only person to explain why d* gave OP unexpected results. But two concerns: (1) Isn't the regex d.* equivalent to simply d? (2) Wildcard d* implies the input should start with d, whereas regex d.* permits d anywhere in the input.
  • scrat.squirrel
    scrat.squirrel about 10 years
    I did not downvote, but I can see your mistake: ^ char in regex means match at beginning of line... which is not equivalent to wildcard pattern such as "d*". So this is misleading.
  • Mark Lakata
    Mark Lakata about 10 years
    @woohoo - The glob pattern d* will only match file names that begin with d. The regular expression /^d/ will only match strings that begin with d. They are the same. The ^ pattern in regexs can mean start of string or start of line (i.e. after \n) depending on the regex options but that's not applicable here, since the inputs are just strings.
  • Wim Coenen
    Wim Coenen almost 10 years
    I think there will be a problem here if escaping is allowed in the wildcard pattern, e.g. if you want to match the * character, then the wildcard pattern for that would be \*. This would then be transformed into the regex \\.* which does something different.
  • Stéphane Gourichon
    Stéphane Gourichon over 9 years
    The question was not really explicit whether it was about just matching a string against a pattern (probably) or against current directory. And indeed the latter case is awful because Windows silently matches against the pattern not only the visible file names but also the hidden DOS 8.3 file names, which sometime makes unexpected matches!
  • user425678
    user425678 over 9 years
    You need to add a reference to "Microsoft.VisualBasic"
  • dbkk
    dbkk about 9 years
    A major problem is that this does not escape other regex special characters, so something like a+.b* (a valid filename) would match improperly.
  • dprothero
    dprothero over 8 years
    @dbkk Tthe code is calling Regex.Escape first, so it will match your example fine.
  • MrZweistein
    MrZweistein almost 3 years
    I think the translation of the wildcard "?" should be different as for example *.txt?would produce no matches on any file with .txt extensions. I suggest the following code instead that would catch those cases: string regex = Regex.Escape(wildcard).Replace(@"\*", ".*").Replace(@"\?", ".?"); return "^" + regex + "$";