Home > database >  IF with multi MATCH Comparison Operators
IF with multi MATCH Comparison Operators

Time:01-29

I facing with create shortest code for matching multiple words in single line. My script file name could have long random name, but its always contains part of bellow names quoted in code.

This bellow code unfortunately does't work fine:

$ScriptFileName = (Get-Item $PSCommandPath).Basename

If (($ScriptFileName -match "Report", "Global", "Monthly")) {
    Write-Output "All days"
} ElseIF (($ScriptFileName -match "Day", "Hour", "Present")) {
    Write-Output "Today"
} Else {
    Write-Output "Other file name"}
    
Pause

Output of this code is always caching last Else - "Other file name". I think my problem here is -match isn't best operator for part of multiple file names.

Working code with -match is like that:

$ScriptFileName = (Get-Item $PSCommandPath).Basename

If (($ScriptFileName -match "Reaport") -or ($ScriptFileName -match "Global") -or ($ScriptFileName -match "Monthly")) {
    Write-Output "All days"
} ElseIF (($ScriptFileName -match "Day") -or ($ScriptFileName -match "Hour") -or ($ScriptFileName -match "Present")) {
    Write-Output "Today"
} Else {
    Write-Output "Other file name"}
    
Pause

But is quite annoying to write it like this way :|

I also tried with operator: "-in", but its can't match only part of file name, right?

Can someone have some better idea - is this can be in possible shortest and clearer way written?

CodePudding user response:

Allowing multiple patterns as the RHS of -like and -match - so as to return $true if any of them match - is not implemented in PowerShell as of 7.3.2, but would be a helpful future improvement - GitHub issue #2132 asks for just that.


Olaf has provided the crucial pointer: use the regex alternation construct |, to match multiple patterns using with a single regex string:

if (($ScriptFileName -match 'Report|Global|Monthly')) { # ...

# With an array variable as input, using the -join operator.
$patterns = "Report", "Global", "Monthly"
if (($ScriptFileName -match ($patterns -join '|'))) { # ...

The alternative is to use Select-String, which does support multiple patterns; with the -Quiet switch, it returns $true or $false, like -match:[1]

# You may even omit quoting around the patterns in this simple case.
if ($ScriptFileName | Select-String -Quiet "Report", "Global", "Monthly") { # ...

Note: -Quiet additionally ensures that matching against multiple inputs stops once a match is found.


The Select-String approach, while slower than -match, has the advantage that you can opt to have the patterns interpreted as literal substrings rather than as regexes, namely with with the -SimpleMatch switch:

# -SimpleMatch treats the patterns as *literals*.
# Without it, an error would occur, because the patterns aren't valid *regexes*.
'A * is born', 'A ', 'No match' | Select-String -SimpleMatch *,  

By contrast, -match invariably treats its RHS string operand as a regex, so if you want to treat the search strings as literal substrings, you'll have to \-escape regex metachacharacters, such as ., * and , or call [regex]::Escape() on the string as a whole:

# With metacharacter-individual \-escaping:
# Note that -match, as all comparison operators, can operate on an 
# *array of inputs*, in which case it acts as a *filter* and returns
# a *subarray of matching strings* (and the automatic $Matches variable is then 
# *not* populated).
'A * is born', 'A ', 'No match' -match (('\*', '\ ') -join '|')

# With whole-pattern escaping:
'A * is born', 'A ', 'No match' -match (('*', ' ').ForEach({ [regex]::Escape($_) }) -join '|')

Further considerations:

  • Unfortunately, PowerShell has no operator for literal substring matching (but does offer Select-String -SimpleMatch, as discussed): -match uses regexes, -like uses wildcard expressions) and matches input strings in full. Escaping is needed in order for these operators to treat a pattern as a literal string (as already shown for -match above); to do this programmatically is cumbersome:

    $pattern = '*' 
    
    # Same as:
    #  'A * is born' -match '\*'
    'A * is born' -match [regex]::Escape($pattern)
    
    # Same as:
    #  'A * is born' -like '*`**'
    'A * is born' -like ('*{0}*' -f [wildcardPattern]::Escape($pattern))
    
  • The -contains operator - despite its similarity in name to the [string] type's .Contains() method - does not perform substring matching and instead tests whether a collection/array contains a given (single) element in full. The -in operator behaves the same, albeit with reversed operands:

     # -> $true, but only because search string 'two' 
     # matches an array element *in full*.
     'one', 'two', three' -contains 'two' 
    
     # Equivalent -in operation.
     'two' -in 'one', 'two', three'
    
  • Using the .Contains() method - available on a single input string and with a single substring to search for - is an option, but this method is invariably case-sensitive in Windows PowerShell; in PowerShell (Core) 7 , case-insensitivity is available on demand (whereas it is the default for PowerShell's operators, which offer c-prefixed variants such as -cmatch for case-sensitivity). The workaround for Windows PowerShell is to use the .IndexOf() method:

    $substring = '* is' 
    
    # Case-SENSITIVE
    'A * is born'.Contains($substring)
    
    # Case-INSENSITIVE
    
    # PowerShell 7  ONLY:
    #  'InvariantCultureIgnoreCase' is auto-converted to
    #  [StringComparison]::InvariantCultureIgnoreCase in this case.
    'A * IS BORN'.Contains($substring, 'InvariantCultureIgnoreCase')
    
    # Windows PowerShell workaround, via .IndexOf()
    -1 -ne 'A * IS BORN'.IndexOf($substring, [StringComparison]::InvariantCultureIgnoreCase)
    
  • Select-String -SimpleMatch is the superior alternative, given that it supports multiple search strings as well as multiple input strings and also works in both PowerShell editions, case-insensitively by default (as PowerShell is in general), with a -CaseSensitive opt-in (still, introducing a substring-matching operator as well would help, both for performance and brevity):

    # Single input string, single search string.
    # Add -CaseSensitive for case-sensitivity.
    # -Quiet returns $true if the match succeeds.
    $substring = '* is'
    'A * IS BORN' | Select-String -Quiet -SimpleMatch $substring
    
    # Multiple input strings, multiple search strings.
    # Due to NOT using -Quiet, all matches are returned, similar
    # to the filtering behavior of -match with an array LHS.
    # `ForEach-Object Line` returns the matching *strings* only,
    # by extracting the .Line property value from the match-info objects.
    $substrings = '* is', ' A '
    'A * IS BORN', 'Got an A ', 'Not me' | 
      Select-String -SimpleMatch $substrings | 
      ForEach-Object Line
    
    # Simpler PowerShell 7 -only alternative:
    # The -Raw switch returns the matching input strings directly.
    $substrings = '* is', ' A '
    'A * IS BORN', 'Got an A ', 'Not me' | 
      Select-String -SimpleMatch -Raw $substrings
    

[1] Strictly speaking, if there is no match, nothing is returned, but in a Boolean context such as an if statement that is equivalent to $false.

CodePudding user response:

Arrays aren't meant to be on the right side, but on the left is fine:

'Report', 'Global', 'Monthly' -match 'L'

Global
Monthly

I've seen this question with -match or -like few times. People want this. But it converts the right side to one string, and this ends up true:

if ('Report Global Monthly' -match 'Report', 'Global', 'Monthly') {
  $true
}

True

Select-string can take an array of patterns. You can use "select-string -quiet", but any result ends up being true anyway.

if('Report' | select-string 'Report', 'Global', 'Monthly') {
  $true
}

True
  • Related