Search code examples
powershellsubstringstring-matching

search a substring with special characters in a string


I am searching a substring with special characters in a string. How do I search for the substring in the string.

$path = 'c:\test'
$mountpoint = 'c:\test\temp\20190987-120\'

I want to search for the $path in $mountpoint

I have tried using -match,-contains,-in etc..

PS C:\>$path = 'c:\test'
PS C:\>$mountpoint = 'c:\test\temp\20190987-120\'
PS C:\>$path -contains $mountpoint
False

Solution

  • AdminOfThing's answer is helpful, but I found myself wanting things to be framed differently.

    • You're looking for a way to perform a literal substring search that is anchored at the start, which is only indirectly supported in PowerShell - see the next section for solutions.

    • Operators -contains and -in are unrelated to substring matching (despite the similarity in name between -contains and the String.Contains() .NET method).

      • They test a single value's membership (being contained as a whole) in a collection, by way of element-by-element equality comparisons (implied -eq). See the docs and the bottom section of this answer for details.

      • If you want to combine the two tasks - looking for a substring in all elements of a collection - you can take advantage of the fact that PowerShell's -match and -like operators - discussed below - can operate on collection-valued LHSs as well, in which case they act as filters; while this is not exactly the same as testing for membership, it can effectively be used for that; this answer shows how to use -match that way.


    Solutions:

    Using the .NET framework:

    The .NET String.IndexOf() method performs literal substring searching and returns the 0-based index of the character where the substring starts in the input string (and -1 if the substring cannot be found at all):

    # -> $true
    0 -eq 'foo\bar'.IndexOf('foo\')
    

    Note that, unlike PowerShell's operators, the above is case-sensitive by default, but you can change to case-insensitive behavior with additional arguments:

    # -> $true
    0 -eq 'foo\bar'.IndexOf('FOO\', [StringComparison]::InvariantCultureIgnoreCase)
    

    Note that PowerShell uses the invariant rather than the current culture in many (but not all) contexts, such as with operators -eq, -contains, -in and the switch statement.

    If there were no need to anchor your substring search, i.e., if you only want to know whether the substring is contained somewhere in the input string, you can use String.Contains():

     # Substring is present, but not at the start
     # Note: matching is case-SENSITIVE.
     # -> $true
     'foo\bar'.Contains('oo\')   
    

    Caveat: In Windows PowerShell, .Contains() is invariably case-sensitive. In PowerShell (Core) 7+, an additional overload is available that offers case-insensitivity (e.g., 'FOO\BAR'.Contains('oo\', 'InvariantCultureIgnoreCase'))


    Using the -match operator:

    While -match does implicitly perform substring matching, it does so based on a regex (regular expression) rather than a literal string.

    -match performs case-insensitive matching by default; use the -cmatch variant for case-sensitivity.

    This means that you can conveniently use ^, the start-of-input anchor, to ensure that the search expression only matches at the start of the input string.

    Conversely, in order for your search string to be treated as a literal string in your regex, you must \-escape any regex metacharacters in it (characters that have special meaning) in a regex.

    Since \ is therefore itself a metacharacter, it must be escaped too, namely as \\.

    In string literals you can do the escaping manually:

    # Manual escaping: \ is doubled.
    # Note the ^ to anchor matching at the start.
    # -> $true
    'foo\bar' -match '^foo\\'
    

    Programmatically, when the string as a variable, you must use the [regex]::Escape() method:

    # Programmatic escaping via [regex]::Escape()
    # Note the ^ to anchor matching at the start.
    # -> $true
    $s = 'foo\'; 'foo\bar' -match ('^' + [regex]::Escape($s))
    

    Using the -like operator:

    Unlike -match, -like performs full-string matching and does so based on wildcard expressions (a.k.a globs in the Unix world); while distantly related to regexes, they use simpler, incompatible syntax (and are far less powerful).

    -like performs case-insensitive matching by default; use the -clike variant for case-sensitivity.

    Wildcards have only 3 basic constructs and therefore only 4 metacharacters: ? (to match a single char.), * (to match any number of chars., including none), and [ (the start of a character set or range matching a single char., e.g., [a-z] or [45]), and the escape character, `

    In the simplest case, you can just append * to your search string to see if it matches at the start of the input string:

    # OK, because 'foo\' contains none of: ? * [ `
    # -> $true
    'foo\bar' -like 'foo\*'
    
    # With a variable, using an expandable string:
    # -> $true
    $s = 'foo\'; 'foo\bar' -like "$s*"
    

    As with -match, however, programmatic escaping may be necessary, which requires a call to [WildcardPattern]::Escape():

    # -> $true
    $s = 'foo['; 'foo[bar' -like ([WildcardPattern]::Escape($s) + '*')