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 offerc
-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