Check if a string exists in an array even as a substring in PowerShell - powershell

I'm trying to work out if a string exists in an array, even if it's a substring of a value in the array.
I've tried a few methods and just can't get it to work, not sure where I'm going wrong.
I have the below code, you can see that $val2 exists within $val1, but I always get a FALSE when I run it.
$val1 = "folder1\folder2\folder3"
$val2 = "folder1\folder2"
$val3 = "folder9"
$val_array = #()
$val_array += $val1
$val_array += $val3
$null -ne ($val_array | ? { $val2 -match $_ }) # Returns $true
I also tried:
foreach ($item in $val_array) {
if ($item -match $val2) {
Write-Host "yes"
}
}

The -Match operator does a regular expression comparison. Where the backslash character (\) has a special meaning (it escapes the following character).
Instead you might use the -Like operator:
$val_array -Like "*$val2*"
Yields:
folder1\folder2\folder3

iRon's helpful answer offers the best solution to your problem, using wildcard matching via the -like operator.
Note:
The need to escape select characters in a search pattern in order for the pattern to be taken verbatim in principle also applies to the wildcard-based -like operator, not just to the regex-based -match operator, but since wildcard expressions have far fewer metacharacters than regexes - namely just *, ?, and [ - the need for such escaping doesn't often arise in practice; whereas regexes require \ as the escape characters, wildcards use `, and programmatic escaping can be achieved with [WildcardPattern]::Escape()
Unfortunately, as of PowerShell 7.2, there is no dedicated operator for verbatim substring matching:
A workaround for this limitation is to call the [string] .NET type's .Contains() method (on a single input string only), however, this performs case-sensitive matching, whereas PowerShell operators are case-insensitive by default, but offer case-sensitive variants simply by prefixing the operator name with c (e.g., -clike, -cmatch).
In Windows PowerShell, .Contains() is invariably case-sensitive, but in PowerShell (Core) 7+ an additional overload is available that offers case-insensitive matching:
'Foo'.Contains('fo') # -> $false, due to case difference
# PowerShell (Core) 7+ *only*:
'Foo'.Contains('fo', 'InvariantCultureIgnoreCase') # -> $true
Caveat: Despite the name similarity, PowerShell's -contains operator does not perform substring matching; instead, it tests whether a collection contains a given element (in full).
As for what you tried:
Your primary problem is that you've accidentally swapped the -match operator's operands: the search pattern - which is invariably interpreted as a regex (regular expression) - must be on the RHS.
As iRon points out, in order for your search pattern to be taken verbatim (literally), you need to escape regex metacharacters with \, and the robust, programmatic way to do this is with [regex]::Escape().
Therefore, the immediate fix would have been (? is a built-in alias of the Where-Object cmdlet):
# OK, but SLOW.
$val_array | ? { $_ -match [regex]::Escape($val2) }
However, this solution is inefficient (it involves the pipeline and a cmdlet).
Fortunately, PowerShell's comparison operators can be applied to arrays (collections) directly, in which case they act as filters, i.e. they return the sub-array of matching elements - see the docs.
iRon's answer uses this technique with -like, but it equally works with -match, so that your expression can be simplified to the following, much more efficient form:
# MUCH FASTER.
$val_array -match [regex]::Escape($val2)

Try the string method Contains:
$null -ne ($val_array | ? { $_.Contains($val2) })

Related

Powershell match similar entries in an array

I've written myself a script to check for vm-folders in vmware vcenter that dont match the corresponding vmname.
There are a few automatically deployed VMs which i need to exclude from this check. Those VMs are always similarly named, but with an incremented number at the end. I've declared an array $Vmstoginrore containing strings of them and i'm trying to match my $VmName with this array but it does not work. Ive also tried it with like but i cannot seem to get this to work.
$Vmstoignore=#( "Guest Introspection","Trend Micro Deep Security")
$VmName = "Guest Introspection (4)"
if ($Vmstoignore-match $VmName ){
Write-Output "does match"
}
else {
Write-Output "doesn't match"
}
As of v7.2.x, PowerShell offers no comparison operators that accept an array of comparison values (only the input operand - the LHS - is allowed to be an array).
However, sine the -match operator is regex-based, you can use a single regex with an alternation (|) to match multiple patterns.
Note that the regex pattern to match against must be the RHS (right-hand side) operand of -match (e.g. 'foo' -match '^f' - in your question, the operands are mistakenly reversed).
The following code shows how to construct the regex programmatically from the given, literal array elements (VM name prefixes):
# VM name prefixes to match.
$Vmstoignore = #("Guest Introspection", "Trend Micro Deep Security")
# Construct a regex with alternation (|) from the array, requiring
# the elements to match at the *start* (^) of the input string.
# The resulting regex is:
# ^Guest\ Introspection|^Trend\ Micro\ Deep\ Security
$regex = $Vmstoignore.ForEach({ '^' + [regex]::Escape($_) }) -join '|'
# Sample input name.
$VmName = "Guest Introspection (4)"
# Now match the input name against the regex:
# -> $true
$VmName -match $regex
Note:
You may alternatively construct the regex directly as a string, in which case you need to manually \-escape any regex metacharacters, such as . (no escaping is required with your sample array):
$regex = '^Guest Introspection|^Trend Micro Deep Security'
Note that [regex]::Escape() escapes (spaces) as \ , even though spaces aren't metacharacters. However, if the x (IgnorePatternWhiteSpace) regex option is in effect (e.g. by placing (?x) at the start of the regex), spaces that are a meaningful part of the pattern do require this escaping. In the absence of this option (it is off by default), escaping spaces is not necessary.
For a detailed explanation of the regex and the ability to interact with it, see this regex101.com page.

Powershell if String.StartsWith() multiple strings

I am very new to PowerShell and i am trying run some code if a string does not start with a certain character, however i can not get this to work with multiple characters.
This is the code that works fine.
if (-Not $recdata.StartsWith("1"))
{
//mycode.
}
But what i want is multiple checks like this
if (-Not $recdata.StartsWith("1") -Or -Not $recdata.StartsWith("2"))
{
//mycode.
}
But this does not work it breaks the whole function eventhough powershell does not throw any errors. I have tried multiple things but i cant find any solution
MundoPeter has pointed out the logic flaw in your approach - -or should be -and - and Santiago Squarzon has offered an alternative based on the regex-based -match operator.
Let me offer the following PowerShell-idiomatic solutions, taking advantage of the fact that PowerShell's operators offer negated variants simply by prepending not to their names:
$recdata[0] -notin '1', '2' # check 1st char of LHS against RHS array
$recdata -notlike '[12]*' # check LHS against wildcard expression
$recdata -notmatch '^[12]' # check LHS against regex
See also:
-in, the is-the-LHS-contained-in-the-RHS-collection operator
-like, the wildcard matching operator
-match, the regular-expression matching operator

Issue with Powershell update string in each file within a folder

I have a set of SQL files stored in a folder. These files contain the schema name in the format dbo_xxxxxx where xxxxxx is the year & month e.g. dbo_202001, dbo_202002 etc. I want the powershell script to replace the xxxxx number with a new one in each of these SQL files. I'm using the below script to achieve that. However, the issue is that it seems to partially match on the old string (instead of matching on the full string) and puts the new string in place e.g. instead of replacing [dbo_202001] with [dbo_201902], anywhere it finds d, b, o etc. it replaces it with [dbo_201902]. Anyway to fix this?
$sourceDir = "C:\SQL_Scripts"
$SQLScripts = Get-ChildItem $sourceDir *.sql -rec
foreach ($file in $SQLScripts)
{
(Get-Content $file.PSPath) |
Foreach-Object { $_ -replace "[dbo_202001]", "[dbo_201902]" } |
Set-Content $file.PSPath -NoNewline
}
vonPryz and marsze have provided the crucial pointers: since the -replace operator operates on regexes (regular expressions), you must \-escape special characters such as [ and ] in order to treat them verbatim (as literals):
$_ -replace '\[dbo_202001\]', '[dbo_201902]'
While use of the -replace operator is generally preferable, the [string] type's .Replace() method directly offers verbatim (literal) string replacements and is therefore also faster than -replace.
Typically, this won't matter, but in situations similar to yours, where many iterations are involved, it may (note that the replacement is case-sensitive):
$_.Replace('[dbo_202001]', '[dbo_201902]')
See the bottom section for guidance on when to use -replace vs. .Replace().
The performance of your code can be greatly improved:
$sourceDir = 'C:\SQL_Scripts'
foreach ($file in Get-ChildItem -File $sourceDir -Filter *.sql -Recurse)
{
# CAVEAT: This overwrites the files in-place.
Set-Content -NoNewLine $file.PSPath -Value `
(Get-Content -Raw $file.PSPath).Replace('[dbo_202001]', '[dbo_201902]')
}
Since you're reading the whole file into memory anyway, using Get-Content's -Raw switch to read it as a single, multi-line string (rather than an array of lines) on which you can perform a single .Replace() operation is much faster.
Set-Content's -NoNewLine switch is needed to prevent an additional newline from getting appended on writing back to the file.
Note the use of the -Value parameter rather than the pipeline to provide the file content. Since there's only a single string object to write here, it makes little difference, but in general, with many objects to write that are already collected in memory, Set-Content ... -Value $array is much faster than $array | Set-Content ....
Guidance on use of the -replace operator vs. the .Replace() method:
-replace is the PowerShell-specific regex (regular-expression)-based string replacement operator,
whereas .Replace() is a method of the .NET [string] type. (System.String), which performs verbatim (literal) string replacements.
Note that both features invariably replace all matches they find, and, conversely, return the original string if none are found.
Generally, PowerShell's -replace operator is a more natural fit in PowerShell code - both syntactically and due to its case-insensitivity - and offers more functionality, thanks to being regex-based.
The .Replace() method is limited to verbatim replacements and in Windows PowerShell to case-sensitive ones, but has the advantage of being faster and not having to worry about escaping special characters in its arguments:
Only use the [string] type's .Replace() method:
for invariably verbatim string replacements
with the following case-sensitivity:
PowerShell [Core] v6+: case-sensitive by default, optionally case-insensitive via an additional argument; e.g.:
'FOO'.Replace('o', '#', 'InvariantCultureIgnoreCase')
Windows PowerShell: invariably(!) case-sensitive
if functionally feasible, when performance matters
Otherwise, use PowerShell's -replace operator (covered in more detail here):
for regex-based replacements:
enables sophisticated, pattern-based matching and dynamic construction of replacement strings
To escape metacharacters (characters with special syntactic meaning) in order to treat them verbatim:
in the pattern (regex) argument: \-escape them (e.g., \. or \[)
in the replacement argument: only $ is special, escape it as $$.
To escape an entire operand in order to treat its value verbatim (to effectively perform literal replacement):
in the pattern argument: call [regex]::Escape($pattern)
in the replacement argument: call $replacement.Replace('$', '$$')
with the following case-sensitivity:
case-insensitive by default
optionally case-sensitive via its c-prefixed variant, -creplace
Note: -replace is a PowerShell-friendly wrapper around the [regex]::Replace() method that doesn't expose all of the latter's functionality, notably not its ability to limit the number of replacements; see this answer for how to use it.
Note that -replace can directly operation on arrays (collections) of strings as the LHS, in which case the replacement is performed on each element, which is stringified on demand.
Thanks to PowerShell's fundamental member-access enumeration feature, .Replace() too can operate on arrays, but only if all elements are already strings. Also, unlike -replace, which always also returns an array if the LHS is one, member-access enumeration returns a single string if the input object happens to be a single-element array.
As an aside: similar considerations apply to the use of PowerShell's -split operator vs. the [string] type's .Split() method - see this answer.
Examples:
-replace - see this answer for syntax details:
# Case-insensitive replacement.
# Pattern operand (regex) happens to be a verbatim string.
PS> 'foo' -replace 'O', '#'
f##
# Case-sensitive replacement, with -creplace
PS> 'fOo' -creplace 'O', '#'
f#o
# Regex-based replacement with verbatim replacement:
# metacharacter '$' constrains the matching to the *end*
PS> 'foo' -replace 'o$', '#'
fo#
# Regex-based replacement with dynamic replacement:
# '$&' refers to what the regex matched
PS> 'foo' -replace 'o$', '>>$&<<'
fo>>o<<
# PowerShell [Core] only:
# Dynamic replacement based on a script block.
PS> 'A1' -replace '\d', { [int] $_.Value + 1 }
A2
# Array operation, with elements stringified on demand:
PS> 1..3 -replace '^', '0'
01
02
03
# Escape a regex metachar. to be treated verbatim.
PS> 'You owe me $20' -replace '\$20', '20 dollars'
You owe me 20 dollars.
# Ditto, via a variable and [regex]::Escape()
PS> $var = '$20'; 'You owe me $20' -replace [regex]::Escape($var), '20 dollars'
You owe me 20 dollars.
# Escape a '$' in the replacement operand so that it is always treated verbatim:
PS> 'You owe me 20 dollars' -replace '20 dollars', '$$20'
You owe me $20
# Ditto, via a variable and [regex]::Escape()
PS> $var = '$20'; 'You owe me 20 dollars' -replace '20 dollars', $var.Replace('$', '$$')
You owe me $20.
.Replace():
# Verbatim, case-sensitive replacement.
PS> 'foo'.Replace('o', '#')
f##
# No effect, because matching is case-sensitive.
PS> 'foo'.Replace('O', '#')
foo
# PowerShell [Core] v6+ only: opt-in to case-INsensitivity:
PS> 'FOO'.Replace('o', '#', 'InvariantCultureIgnoreCase')
F##
# Operation on an array, thanks to member-access enumeration:
# Returns a 2 -element array in this case.
PS> ('foo', 'goo').Replace('o', '#')
f##
g##
# !! Fails, because not all array elements are *strings*:
PS> ('foo', 42).Replace('o', '#')
... Method invocation failed because [System.Int32] does not contain a method named 'Replace'. ...

PowerShell to check the string in the array [duplicate]

I am trying to filter out users that are in a specific group.
I got the following output in a variable:
Group1
Group2
etc...
One group for each line saved in an array. Im trying to filter out only one specific group. But when I use -contains it always says $false, even tho the group is there.
My Code:
$group = get-aduser -identity name -properties memberof |
select-object -expandproperty memberof | %{ (get-adgroup $_).name }
$contains = $group -contains "string"
$contains is $false even if the array has elements that contain the string...
What am I missing?
It looks like your misconception was that you expected PowerShell's -contains operator to perform substring matching against the elements of the LHS array.
Instead, it performs equality tests - as -eq would - against the array's elements - see this answer for details.
In order to perform literal substring matching against the elements of an array, use:
# With non-literal search strings:
[bool] $contains = $group -match ([regex]::Escape($someString))
# With a string literal that doesn't contain regex metachars.,
# escaping isn't needed.
[bool] $contains = $group -match 'foo'
# With a string literal with metachars., you must individually \-escape them.
[bool] $contains = $group -match 'foo\.bar'
Note:
The above shows a robust, generic way of ensuring that your search string is treated as a literal value using [regex]::Escape(), which is necessary because -match expects a regex (regular expression) as its RHS (the search pattern).
Escaping isn't always necessary; specifically, only the presence of so-called metacharacters (those with special meaning in a regex, such as .) requires it, and when you're using a string literal, you can opt to directly \-escape them; e.g., to search for literal substring a.b, you can pass 'a\.b'.
Chances are that AD group names do not require escaping, but it's important to be aware of the need for it in general.
As with all operators in PowerShell, by default the matching is case-insensitive; use the -cmatch variant for case-sensitive matching.
The [bool] type constrained above is used to ensure that the result of the -match operation is converted to a Boolean:
While -match directly returns a Boolean with a scalar (non-array) LHS, with an array LHS it acts as a filter, and returns the matching array elements instead; interpreted in a Boolean context, such as in an if conditional, that usually still gives the expected result, because a non-empty array is interpreted as $true, whereas an empty one as $false; again, however it's important to know the difference.
This will rarely be a performance concern in practice, but it is worth noting that -match, due to acting as a filter with arrays, always matches against all array elements - it doesn't stop once the first match is found, the way that the -contains and -in operators do.
On the plus side, you can use -match to obtain the matching elements themselves.
The mistaken expectation of -contains performing substring matching may have arisen from confusion with the similarly named, but unrelated String.Contains() method, which indeed performs literal substring matching; e.g., 'foo'.Contains('o') yields $true.
Also note that .Contains() is case-sensitive - invariably in Windows PowerShell, by default in PowerShell (Core) 7+.
PowerShell has no operator for literal substring matching.
However, you could combine PowerShell's generic array-filtering features with the .Contains() string method - but note that this will typically perform (potentially much) worse than the -match approach.
A reasonably performant alternative is to use the PSv4+ .Where() array method as follows:
# Note: Substring search is case-sensitive here.
[bool] $contains = $group.Where({ $_.Contains("string") }, 'First')
On the plus side, this approach stops matching once the first match is found.
The answer was -match insted of contains. Now the output is true.

Arrays and -contains - test for substrings in the elements of an array

I am trying to filter out users that are in a specific group.
I got the following output in a variable:
Group1
Group2
etc...
One group for each line saved in an array. Im trying to filter out only one specific group. But when I use -contains it always says $false, even tho the group is there.
My Code:
$group = get-aduser -identity name -properties memberof |
select-object -expandproperty memberof | %{ (get-adgroup $_).name }
$contains = $group -contains "string"
$contains is $false even if the array has elements that contain the string...
What am I missing?
It looks like your misconception was that you expected PowerShell's -contains operator to perform substring matching against the elements of the LHS array.
Instead, it performs equality tests - as -eq would - against the array's elements - see this answer for details.
In order to perform literal substring matching against the elements of an array, use:
# With non-literal search strings:
[bool] $contains = $group -match ([regex]::Escape($someString))
# With a string literal that doesn't contain regex metachars.,
# escaping isn't needed.
[bool] $contains = $group -match 'foo'
# With a string literal with metachars., you must individually \-escape them.
[bool] $contains = $group -match 'foo\.bar'
Note:
The above shows a robust, generic way of ensuring that your search string is treated as a literal value using [regex]::Escape(), which is necessary because -match expects a regex (regular expression) as its RHS (the search pattern).
Escaping isn't always necessary; specifically, only the presence of so-called metacharacters (those with special meaning in a regex, such as .) requires it, and when you're using a string literal, you can opt to directly \-escape them; e.g., to search for literal substring a.b, you can pass 'a\.b'.
Chances are that AD group names do not require escaping, but it's important to be aware of the need for it in general.
As with all operators in PowerShell, by default the matching is case-insensitive; use the -cmatch variant for case-sensitive matching.
The [bool] type constrained above is used to ensure that the result of the -match operation is converted to a Boolean:
While -match directly returns a Boolean with a scalar (non-array) LHS, with an array LHS it acts as a filter, and returns the matching array elements instead; interpreted in a Boolean context, such as in an if conditional, that usually still gives the expected result, because a non-empty array is interpreted as $true, whereas an empty one as $false; again, however it's important to know the difference.
This will rarely be a performance concern in practice, but it is worth noting that -match, due to acting as a filter with arrays, always matches against all array elements - it doesn't stop once the first match is found, the way that the -contains and -in operators do.
On the plus side, you can use -match to obtain the matching elements themselves.
The mistaken expectation of -contains performing substring matching may have arisen from confusion with the similarly named, but unrelated String.Contains() method, which indeed performs literal substring matching; e.g., 'foo'.Contains('o') yields $true.
Also note that .Contains() is case-sensitive - invariably in Windows PowerShell, by default in PowerShell (Core) 7+.
PowerShell has no operator for literal substring matching.
However, you could combine PowerShell's generic array-filtering features with the .Contains() string method - but note that this will typically perform (potentially much) worse than the -match approach.
A reasonably performant alternative is to use the PSv4+ .Where() array method as follows:
# Note: Substring search is case-sensitive here.
[bool] $contains = $group.Where({ $_.Contains("string") }, 'First')
On the plus side, this approach stops matching once the first match is found.
The answer was -match insted of contains. Now the output is true.