Contains operator not working in Powershell - powershell

I have never got the -contains operator to work in Powershell I don't know why.
Here's an example of where it isn't working. I use -like in its place but I'd love it if you could tell me why this isn't working.
PS HKLM:\Software\Microsoft\Windows NT\CurrentVersion> (gp . P*).ProductName
Windows 10 Enterprise
PS HKLM:\Software\Microsoft\Windows NT\CurrentVersion> (gp . P*).ProductName -contains "Windows"
False
PS HKLM:\Software\Microsoft\Windows NT\CurrentVersion> (gp . P*).ProductName | gm | select TypeName | Get-Unique
TypeName
--------
System.String

The -contains operator is not a string operator, but a collection containment operator:
'a','b','c' -contains 'b' # correct use of -contains against collection
From the about_Comparison_Operators help topic:
Type Operator Description
Containment -contains Returns true when reference value contained in a collection
-notcontains Returns true when reference value not contained in a collection
-in Returns true when test value contained in a collection
-notin Returns true when test value not contained in a collection
Usually you would use the -like string operator in PowerShell, which supports Windows-style wildcard matching (* for any number of any characters, ? for exactly one of any character, [abcdef] for one of a character set):
'abc' -like '*b*' # $true
'abc' -like 'a*' # $true
Another alternative is the -match operator:
'abc' -match 'b' # $true
'abc' -match '^a' # $true
For verbatim substring matching, you would want to escape any input pattern, since -match is a regex operator:
'abc.e' -match [regex]::Escape('c.e')
An alternative is to use the String.Contains() method:
'abc'.Contains('b') # $true
With the caveat that, unlike powershell string operators, it's case-sensitive.
String.IndexOf() is yet another alternative, this one allows you to override the default case-sensitivity:
'ABC'.IndexOf('b', [System.StringComparison]::InvariantCultureIgnoreCase) -ge 0
IndexOf() returns -1 if the substring is not found, so any non-negative return value can be interpreted as having found the substring.

The '-contains' operator is best used for comparison to lists or arrays, e.g.
$list = #("server1","server2","server3")
if ($list -contains "server2"){"True"}
else {"False"}
output:
True
I'd suggest using '-match' instead for string comparisons:
$str = "windows"
if ($str -match "win") {"`$str contains 'win'"}
if ($str -match "^win") {"`$str starts with 'win'"}
if ($str -match "win$") {"`$str ends with 'win'"} else {"`$str does not end with 'win'"}
if ($str -match "ows$") {"`$str ends with 'ows'"}
output:
$str contains 'win'
$str starts with 'win'
$str does not end with 'win'
$str ends with 'ows'

Related

IF with multi MATCH Comparison Operators

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?
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 in case of a match, 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] Unlike with -match (with a single-string LHS), if there is no match, nothing is returned, but in a Boolean context such as an if statement that is equivalent to $false.
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

Powershell - Matching text from array and string?

I have an array that simply pulls a list of numbers in one long column. I am trying to match it with some data in a string and if it matches, I am wanting it to state Down otherwise it will state Up in the output CSV.
Here is my code: `
IF($RESULTS -like $TEST)
{$Output = "DOWN"
}ELSE{
$OUtput = "UP"
}
`
$RESULTS is the array, and $TEST is the string. If I do -match it works, but -match only pulls single digits so it gives false positives. For example, if there is a 3 in the list as well as 638 it will mark them both as down. None of the other switches seem to work like -eq, -like, etc.
What am I missing please?
Thanks much for any assistance!
EDIT:
Sample of Data in $TEST
2
3
5
Sample of Output of $RESULTS
5
628
Since 5 exists in both, my expected output would be DOWN and everything else would be UP.
It sounds like you have two arrays of numbers, and you want to test if the input array contains at least one of the values in the test array.
You can use Compare-Object, which compares two arrays and indicates which elements are different and, with -IncludeEqual, also which ones are the same:
if (
(Compare-Object -IncludeEqual $RESULTS $TEST).
Where({ $_.SideIndicator -eq '==' }).
Count -gt 0
) {
$Output = "DOWN"
}
else {
$Output = "UP"
}
As an aside:
You can use an if statement as an expression, which means you only need to specify $Output once:
$Output =
IF (<# conditional #>) {
"DOWN"
}
ELSE {
"UP"
}
In PowerShell (Core) 7+, you can use ?:, the ternary conditional operator for a more concise solution:
$Output = <# conditional #> ? 'DOWN' : 'UP'
I would do it by using "foreach". Hope this might be helpful
foreach ($result in $RESULTS){
if ($result -like $Test){
$OUTPUT = "Down"}
else{
$OUTPUT= "UP"}
}
In your edited question you show that variable $TEST is also an array, so in that case you can do
$TEST = 2,3,5
$RESULTS = 5,628
# compare both arrays for equal items
$Output = if ($TEST | Where-Object { $RESULTS -contains $_ }) {"DOWN"} else {"UP"}
$Output
In this case, $Output will be DOWN because both arrays have the number 5
If however variable $TEST contains a multiline string, then first create an array out of that like
$TEST = #'
2
3
5
'#
# convert the string to array by splitting at the newline
$TEST = $TEST -split '\r?\n' -ne ''
$RESULTS = 5,628
# compare both arrays for equal items
$Output = if ($TEST | Where-Object { $RESULTS -contains $_ }) {"DOWN"} else {"UP"}
$Output

powershell filter filenames with regex

I am building a list of files that I'm putting into my $list variable.
Then I want to filter the list based on the $filter variable. The current solution works, but it doesn't work with a regex.
$filter = #("test.txt","Fake","AnotherFile\d{1..6}")
######### HTML TESTS #############
[string]$list = #"
FakeFile.txt
test120119.txt
AnotherFile120119.txt
LastFile.txt
"#
[array]$files = $list -split '\r?\n'
$files = $files | Where-Object {$_} | Where {$_ -notin $filter} # filter out empty items from the array...
$files
My idea is to put regex patterns in the $filter variable so I can catch filenames that have datestamps in them such as test120119.txt in the $list variable above.
How can I change my code to allow for regex? I tried some variations of select-string without splitting my $list, but was not fruitful. I also tried changing my -notin to -notmatch but this doesn't work at all of course.
If you want to use regex, I think it would be easier to just fully commit to regex with your $filter array.
$filter = "^test\d{0,6}\.txt","^Fake","^AnotherFile\d{0,6}\.txt" -join '|'
$list = #"
FakeFile.txt
test120119.txt
AnotherFile120119.txt
LastFile.txt
"#
$files = $list -split '\r?\n'
$files | Where {$_ -notmatch $filter}
The thing to keep in mind is remembering to escape special regex characters if you want them treated literally. You can use the [regex]::Escape() method to do this for you but not if you already purposely injected regex characters.
Once you have your regex filter list, you can join each item with a regex or using the | character.
Not all operators recognize regex language. -match and -notmatch are among the few that do. -match and -notmatch are not case-sensitive. If you want to match against case, you should use the -c variants of the operators, namely -cmatch and -cnotmatch.
The regex items can be tweaked to your liking. More requirements would need to be given in order to come up with an exact solution. Here are some examples to consider:
\d{0,6} matches 0 to 6 consecutive digits. 122619 will match successfully, but so will 1226. If you want only 0 or 6 digits to match, you can use (\d{6})?.
^ should be used if you want to start each match at the beginning of the input string. So if you want the regex or to apply from the beginning of the string, you need to include ^ in each item or group items succeeding the initial ^ with () accordingly. ^item1|^item2 will return the same capture group 0 match as ^(item1|item2).
\ escape the literal . characters.
Not using anchor characters like ^ and $ create a lot of flexibility and potentially unwanted results. 'FakeFile' -match 'Fake' returns true but so does 'MyFakeFile' -match 'Fake'. However, 'MyFakeFile' -match 'Fake$' returns false and 'MyFake' -match 'Fake$' returns true.

Search Registry and create New-Item

I want to create a New-Item in the registry with a pre check what already exists.
This code
$items = get-item "HKCU:\SOFTWARE\Microsoft\Office\16.0\Excel\Options"
$items.Property -match "OPEN"
returns the following
OPEN
OPEN1
OPEN2
OPEN3
OPEN4
Now I know I need to create a New-Item with the name OPEN5, but how do I count through this? Maybe with a While-Loop?
The most robust approach is to extract the embedded numbers, sort them numerically, and add 1 to the highest index to date:
$nextNdx = 1 +
([int[]] ($items.Property -match '^OPEN' -replace '\D') | Sort-Object)[-1]
$items.Property -match '^OPEN' -replace '\D' returns all property names that start with OPEN and removes all non-digit characters from them (-replace '\D').
[int[]] converts the resulting "number strings" to actual numbers ([int]); note that casting '' or $null to [int] in PowerShell yields 0.
Sort-Object sorts these numbers, and [-1] grabs the last number from the resulting array, i.e., the highest number.
The above is convenient, but not fast, due to use of the pipeline and the Sort-Object cmdlet.
If you want to avoid the pipeline for performance reasons:
$indices = [int[]] ($items.Property -match '^OPEN' -replace '\D')
[Array]::Sort($indices) # sort in place
$nextNdx = 1 + $indices[-1]

Match strings stored in variables using PowerShell

I am attempting to create a backup script that will move files that are older that 30 days, but I want to be able to exclude folders from the list
$a = "C:\\Temp\\Exclude\\test"
$b = "C:\\Temp\\Exclude"
if I run the following:
$a -match $b
Following PowerShell Basics: Conditional Operators -Match -Like -Contains & -In -NotIn:
$Guy ="Guy Thomas 1949"
$Guy -match "Th"
This returns true.
I'd say use wilcards and the like operator, it can save you a lot of head aches:
$a -like "$b*"
The match operator is using regex pattern and the path is having regex special characters in it (the escape characeter). If you still want to use -match - make sure to escape the string:
$a -match [regex]::escape($b)
This will work but keep in mind that it can match in the middle of the string, you can add the '^' anchor to tell the regex engine to match from the begining of the string:
$a -match ("^"+[regex]::escape($b))