How can I use wildcards with IF -contains on an array? - powershell

I'm working on a script backing up the schema for a database to a version control system. I just realized that replicated objects are being scripted out as well; these objects start with sp_MSupd_, sp_MSdel_, sp_MSins_, and syncobj_0x. I would like to skip these items.
I know I can do something like this:
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
If ($ExcludeObjectNames -contains "sp_MSdel_SampleObject")
{
Write-Host "replicated item found"
}
But this only works for exact matches. I tried adding * to the elements in the array but that did not work. And I know I can use -like, but that seems to only be for single values, not arrays.
I know I can do this
$ObjectNames = "sp_MSdel_SampleObject"
If ($ObjectNames -like "sp_MSupd_*" -OR $ObjectNames -like "sp_MSdel_*" -OR $ObjectNames -like "sp_MSins_*" -OR $ObjectNames -like "syncobj_0x*")
{
Write-Host "replicated item found"
}
This is well and fine, BUT if I find more and more things to omit, this becomes more and more ugly.
Is there a way to use wildcards with -contains on an array?

$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
Foreach ($ExcludeObjectName in $ExcludeObjectNames) {
If ("sp_MSdel_SampleObject" -match $ExcludeObjectName)
{
Write-Host "replicated item found"
}
}
Or if you only want to edit variables in the script, you can do:
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
$ObjectToCheck = "sp_MSdel_SampleObject" # This variable contains your original contains target
Foreach ($ExcludeObjectName in $ExcludeObjectNames) {
If ($ObjectToCheck -match $ExcludeObjectName)
{
Write-Host "replicated item found"
}
}
If you don't like looping, you can just build a regex filter from your original array:
$ExcludeObjectNames = #("sp_MSupd_", "sp_MSdel_","sp_MSins_","syncobj_0x")
$ObjectToCheck = "sp_MSdel_SampleObject" # This variable contains your original contains target
$excludeFilter = '(^' + ($excludeobjectnames -join "|^") + ')' # Regex filter that you never have to manually update
If ($ObjectToCheck -match $ExcludeFilter)
{
Write-Host "replicated item found"
}

Related

Powershell .Where() method with multiple properties

I have a GenericList of Hashtables, and I need to test for the existence of a record based on two properties. In my hash table, I have two records that share one property value, but are different on another property value.
Specifically, DisplayName of both is Autodesk Content for Revit 2023
But UninstallString for one is MsiExec.exe /X{GUID} while the other is C:\Program Files\Autodesk\AdODIS\V1\Installer.exe followed by a few hundred characters of other info
I want to select only the one with AdODIS in the UninstallString. And I would like to do it without a loop, and specifically using the .Where() method rather than the pipeline and Where-Object.
There are also MANY other records.
I CAN select just based on one property, like this...
$rawKeys.Where({$_.displayName -eq 'Autodesk Content for Revit 2023'})
And I get the appropriate two records returned. However, when I try expanding that to two properties with different criteria, like this...
$rawKeys.Where({($_.displayName -eq 'Autodesk Content for Revit 2023') -and ($_.uninstallString -like 'MsiExec.exe*')})
nothing is returned. I also tried chaining the .Where() calls, like this...
$rawKeys.Where({$_.displayName -eq 'Autodesk Content for Revit 2023'}).Where({$_.uninstallString -like 'MsiExec.exe*'})
and again, nothing returned.
just to be sure the second condition is working, I tried...
$rawKeys.Where({$_.uninstallString -like 'MsiExec.exe*'})
and got multiple records returned, as expected.
I found [this][1] that talk about doing it with Where-Object, and applying that approach to the method was my first attempt. But I have yet to see either an example of doing it with .Where() or something specifically saying .Where() is limited to one conditional.
So, am I just doing something wrong? Or is this actually not possible with .Where() and I have no choice but to use the pipeline? And there I would have thought based on that link that some variation on...
$rawKeys | Where-Object {(($_.displayName -eq 'Autodesk Content for Revit 2023') -and ($_.uninstallString -like 'MsiExec.exe*'))}
would work, but that's failing too.
I also tried...
$rawKeys.Where({$_.displayName -eq 'Autodesk Content for Revit 2023'}) -and $rawKeys.Where({$_.uninstallString -like 'MsiExec.exe*'})
And THAT returns true, which for my current need is enough, but one: I would like to know if it can be done in a single method call, and two: I can imagine I will eventually want to get the record(s) back, rather than just a bool. Which is only possible with the single method call.
EDIT: OK, this is weird. I tried doing a minimal example of actual data, like this...
$rawKeys = New-Object System.Collections.Generic.List[Hashtable]
$rawKeys.Add(#{
displayName = 'Autodesk Content for Revit 2023'
uninstallString = 'C:\Program Files\Autodesk\AdODIS\V1\Installer.exe whatever else is here'
guid = '{019AEF66-C054-39BB-88AD-B2D8EA9BE40A}'
})
$rawKeys.Add(#{
displayName = 'Autodesk Content for Revit 2023'
uninstallString = 'MsiExec.exe /X{205C6D76-2023-0057-B227-DC6376F702DC}'
guid = '{205C6D76-2023-0057-B227-DC6376F702DC}'
})
and that WORKS. So somewhere in my real code I am changing the data, and for the life of me I can't see where it's happening. But it's happening. The ACTUAL data comes from the registry, with this code...
$uninstallKeyPaths = #('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
$rawKeys = New-Object System.Collections.Generic.List[Hashtable]
$localMachineHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, 0)
foreach ($uninstallKeyPath in $uninstallKeyPaths) {
foreach ($uninstallKeyName in $localMachineHive.OpenSubKey($uninstallKeyPath).GetSubKeyNames()) {
if ($uninstallKeyPath -like '*Wow6432Node*') {
$bitness = 'x32'
} else {
$bitness = 'x64'
}
$uninstallKey = $localMachineHive.OpenSubKey("$uninstallKeyPath\$uninstallKeyName")
if (($displayName = $uninstallKey.GetValue('DisplayName')) -and ($displayVersion = $uninstallKey.GetValue('DisplayVersion')) -and
(($installDate = $uninstallKey.GetValue('InstallDate')) -or ($uninstallString = $uninstallKey.GetValue('UninstallString')))) {
$keyName = [System.IO.Path]::GetFileName($uninstallKey.Name)
$keyData = #{
displayName = $displayName
displayVersion = $displayVersion
guid = "$(if ($keyName -match $pattern.guid) {$keyName})" #$Null
publisher = $uninstallKey.GetValue('Publisher')
uninstallString = $uninstallString
installDate = $installDate
properties = (#($uninstallKey.GetValueNames()) | Sort-Object) -join ', '
type = $bitness
}
[void]$rawKeys.Add($keyData)
}
}
}
So, meaningless unless you actually have Autodesk Revit 2023 installed on your machine, but maybe someone sees where I am changing the data.
[1]: Where-object $_ matches multiple criterias

Nested if and for loop errors

would any of you be able to help me with the below code
$pcname = "jwang"
#We set $test as a string to be called later
$test = get-adcomputer -filter "name -like '$pcname'" | select -ExpandProperty name
#We define a null string to test our IF statement
$nothing = $null
$number = 1
if ($test -ne $null) {
Write-Host "$pcname is currently in use"
for($number = 1; ($test = get-adcomputer -filter "name -like '$pcname + $number'" | select -ExpandProperty name) -ne $null; $number++ ){
Write-Host $pcname + $number
}
}
else {
Write-host "AD lookup complete, new PC name will be $pcname"
}
The IF statement works correctly, but the trouble starts when I add the nested FOR loop.
The end goal is to have AD tell me if the name is available or not.
Right now in AD there is
JWANG
JWANG1
JWANG2
JWANG3
JWANG4
I want the for loop to eventuelly tell me that "JWANG5" is available, any help is appreciated, thank you
The immediate problem with your code is that $pcname + $number - an expression - isn't enclosed in $(...), the subexpression operator inside your expandable (double-quoted) string ("...").
Therefore, use $($pcname + $number) or, relying on mere string interpolation, $pcname$number
An aside re testing for $null:
It is advisable to make $null the LHS for reliably null testing (if ($null -ne $test)), because on the RHS it acts as a filter if the LHS happens to be an array - see about_Comparison_Operators.
However, often - such as when objects are being returned from a command, you can simply use implicit to-Boolean conversion (if ($test)) - see the bottom section of this answer for the complete rules.
Taking a step back:
Querying AD multiple times is inefficient, and since you're already using the -like operator, you can use it with a wildcard pattern to find all matching names at once:
$pcname = 'jwang'
# Note the use of "*" to match all names that *start with* $pcname
$names = get-adcomputer -filter "name -like '$pcname*'" | select -ExpandProperty name
if ($names) { # at least one name found
Write-Host "$pcname is currently in use"
# Find the highest number embedded in the matching names.
$seqNums = [int[]] ($names -replace '\D')
# The new sequence number is the next number following the currently highest.
$nextSeqNum = 1 + [Linq.Enumerable]::Max($seqNums)
# Form the new name and output it.
$pcname + $nextSeqNum
}
else {
Write-host "AD lookup complete, new PC name will be $pcname"
}

Powershell - Checking variable for value in foreach - If no value then log other output

I'm having issue with my foreach method. I am checking in the registry whether a good amount of programs are installed. How would I write it to say something is not installed one time versus it saying something's not installed for each key it checks? Now, If I place a ElseIf it executes "PowerBroker not installed." about 16 times. This is due to it checking every key and writing it out for each key it does not find a match to the displayname. How do I go about it checking the key and only writing it out one time if it's not installed?? Thanks!
$UninstallKeys = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
foreach($Key in $UninstallKeys){
if($Key.GetValue("DisplayName") -Match "BeyondTrust"){
$PBW = $Key.GetValue("DisplayName")
$PBWV = $Key.GetValue("DisplayVersion")
if ($PBW) {
$PBW = $PBW, $PBWV
}
else {
$PBW = "PowerBroker not installed."
$installsmissing = "True"
}
}
Give this script a whirl. If I've understood the requirement correctly it should give you what you need.
$displayName = "BeyondTrust"
$uninstallKeys = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
# Filter the keys down by their display name property
$specificUninstallKeys = $uninstallKeys |
Where-Object {
$_.GetValue("DisplayName") -eq $displayName
}
# Did we find any keys of that name?
if ($specificUninstallKeys) {
Write-Output "Keys found: $($specificUninstallKeys.Length)"
}
else {
Write-Output "Sorry pal, no keys by that name here!"
}
# There may be more than one; hence the loop-y requirement here.
foreach ($specificUninstallKey in $specificUninstallKey) {
Write-Output $displayName
Write-Output $specificUninstallKey.GetValue("DisplayVersion")
}

Pass a single space-delimited string as multiple arguments

I have a Powershell function in which I am trying to allow the user to add or remove items from a list by typing the word "add" or "remove" followed by a space-delimited list of items. I have an example below (slightly edited, so you can just drop the code into a powershell prompt to test it "live").
$Script:ServerList = #("Server01","Server02","Server03")
Function EditServerList (){
$Script:ServerList = $Script:ServerList |Sort -Unique
Write-host -ForegroundColor Green $Script:ServerList
$Inputs = $args
If ($Inputs[0] -eq "start"){
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# EditServerList $Edits
# EditServerList $Edits.split(' ')
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"})
EditServerList start
} Elseif ($Inputs[0] -eq "add"){
$Script:ServerList += $Inputs |where {$_ -NotLike $Inputs[0]}
EditServerList start
} Elseif ($Inputs[0] -eq "remove"){
$Script:ServerList = $Script:ServerList |Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})}
EditServerList start
} Else {
Write-Host -ForegroundColor Red "ERROR!"
EditServerList start
}
}
EditServerList start
As you can see, the function takes in a list of arguments. The first argument is evaluated in the If/Then statements and then the rest of the arguments are treated as items to add or remove from the list.
I have tried a few different approaches to this, which you can see commented out in the first IF evaluation.
I have two problems.
When I put in something like "add Server05 Server06" (without quotes) it works, but it also drops in the word "add".
When I put in "remove Server02 Server03" (without quotes) it does not edit the array at all.
Can anybody point out where I'm going wrong, or suggest a better approach to this?
To address the title's generic question up front:
When you pass an array to a function (and nothing else), $Args receives a single argument containing the whole array, so you must use $Args[0] to access it.
There is a way to pass an array as individual arguments using splatting, but it requires an intermediate variable - see bottom.
To avoid confusion around such issues, formally declare your parameters.
Try the following:
$Script:ServerList = #("Server01", "Server02", "Server03")
Function EditServerList () {
# Split the arguments, which are all contained in $Args[0],
# into the command (1st token) and the remaining
# elements (as an array).
$Cmd, $Servers = $Args[0]
If ($Cmd -eq "start"){
While ($true) {
Write-host -ForegroundColor Green $Script:ServerList
$Edits = Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
#"# Pass the array of whitespace-separated tokens to the recursive
# invocation to perform the requested edit operation.
EditServerList (-split $Edits)
}
} ElseIf ($Cmd -eq "add") {
# Append the $Servers array to the list, weeding out duplicates and
# keeping the list sorted.
$Script:ServerList = $Script:ServerList + $Servers | Sort-Object -Unique
} ElseIf ($Cmd -eq "remove") {
# Remove all specified $Servers from the list.
# Note that servers that don't exist in the list are quietly ignored.
$Script:ServerList = $Script:ServerList | Where-Object { $_ -notin $Servers }
} Else {
Write-Host -ForegroundColor Red "ERROR!"
}
}
EditServerList start
Note how a loop is used inside the "start" branch to avoid running out of stack space, which could happen if you keep recursing.
$Cmd, $Servers = $Args[0] destructures the array of arguments (contained in the one and only argument that was passed - see below) into the 1st token - (command string add or remove) and the array of the remaining arguments (server names).
Separating the arguments into command and server-name array up front simplifies the remaining code.
The $var1, $var2 = <array> technique to split the RHS into its first element - assigned as a scalar to $var1 - and the remaining elements - assigned as an array to $var2, is commonly called destructuring or unpacking; it is documented in Get-Help about_Assignment Operators, albeit without giving it such a name.
-split $Edits uses the convenient unary form of the -split operator to break the user input into an array of whitespace-separated token and passes that array to the recursive invocation.
Note that EditServerList (-split $Edits) passes a single argument that is an array - which is why $Args[0] must be used to access it.
Using PowerShell's -split operator (as opposed to .Split(' ')) has the added advantage of ignoring leading and trailing whitespace and ignoring multiple spaces between entries.
In general, operator -split is preferable to the [string] type's .Split() method - see this answer of mine.
Not how containment operator -notin, which accepts an array as the RHS, is used in Where-Object { $_ -notin $Servers } in order to filter out values from the server list contained in $Servers.
As for what you tried:
EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"}) (a) mistakenly attempts to remove the command name from the argument array, even though the recursive invocations require it, but (b) actually fails to do so, because the RHS of -like doesn't support arrays. (As an aside: since you're looking for exact strings, -eq would have been the better choice.)
Since you're passing the arguments as an array as the first and only argument, $Inputs[0] actually refers to the entire array (command name + server names), not just to its first element (the command name).
You got away with ($Inputs[0] -eq "add") - even though the entire array was compared - because the -eq operator performs array filtering if its LHS is an array, returning a sub-array of matching elements. Since add was among the elements, a 1-element sub-array was returned, which, in a Boolean context, is "truthy".
However, your attempt to weed out the command name with where {$_ -NotLike $Inputs[0]} then failed, and add was not removed - you'd actually have to compare to $Inputs[0][0] (sic).
Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})} doesn't filter anything out for the following reasons:
($Inputs |Where {$_ -Notlike $Inputs[0]}) always returns an empty array, because, the RHS of -Notlike is an array, which, as stated, doesn't work.
Therefore, the command is the equivalent of Where {$_ -NotLike #() } which returns $True for any scalar on the LHS.
Passing an array as individual arguments using splatting
Argument splatting (see Get-Help about_Splatting) works with arrays, too:
> function foo { $Args.Count } # function that outputs the argument count.
> foo #(1, 2) # pass array
1 # single parameter, containing array
> $arr = #(1, 2); foo #arr # splatting: array elements are passed as indiv. args.
2
Note how an intermediate variable is required, and how it must be prefixed with # rather than $ to perform the splatting.
I'd use parameters to modify the ServerList, this way you can use a single line to both add and remove:
Function EditServerList {
param(
[Parameter(Mandatory=$true)]
[string]$ServerList,
[array]$add,
[array]$remove
)
Write-Host -ForegroundColor Green "ServerList Contains: $ServerList"
$Servers = $ServerList.split(' ')
if ($add) {
$Servers += $add.split(' ')
}
if ($remove) {
$Servers = $Servers | Where-Object { $remove.split(' ') -notcontains $_ }
}
return $Servers
}
Then you can call the function like this:
EditServerList -ServerList "Server01 Server02 Server03" -remove "Server02 Server03" -add "Server09 Server10"
Which will return:
Server01
Server09
Server10

Using -notcontains to find substring within string within array

I'm trying to avoid using nested ForEach Loop as part of a larger code. To do this, I'm using the -notcontains operator. Basically, I want to see if a substring exists within a string within an array. If it exists, do nothing, if it does not exist, print "Not Found".
Here is the code...
$arr = #('"value11","value21","value31"','"value12","value22","value32"','"value13","value23","value33"')
if ($arr -notcontains "*`"value24`"*")
{
Write-Host "Not Found"
}
if ($arr -notcontains "*`"value22`"*")
{
Write-Host "Not Found 2"
}
We can see that value24 is not within any strings of the array. However, value22 is within the 2nd string in the array.
Therefor the results should output the following...
Not Found
However, instead I see the following output...
Not Found
Not Found 2
Can anyone tell me why this is happening?
-contains and -notcontains don't operate against patterns.
Luckily, -match and -like and their negative counterparts, when used with an array on the left side, return an array of the items that satisfy the condition:
'apple','ape','vape' -like '*ape'
Returns:
ape
vape
In an if statement, this still works (a 0 count result will be interpreted as $false):
$arr = #('"value11","value21","value31"','"value12","value22","value32"','"value13","value23","value33"')
if ($arr -notlike "*`"value24`"*")
{
Write-Host "Not Found"
}
My take on a solution:
($arr | foreach {$_.contains('"value24"')}) -contains $true
Using the V3 .foreach() method:
($arr.ForEach({$_.contains('"value24"')}).contains($true))
And yet another possibility:
[bool]($arr.where({$_.contains('"value24"')}))
Edit for clearer answer on what I'm looking for...
This is the only way I'm able to figure this out so far. I hope there is a much cleaner solution...
$arr = #('"value11","value21","value31"','"value12","value22","value32"','"value13","value23","value33"')
$itemNotFound = $true
ForEach ($item in $arr)
{
If ($itemNotFound)
{
If ($item -like "*`"value24`"*")
{
$itemNotFound = $false
}
}
}
if ($itemNotFound)
{
Write-Host "Not Found value24"
}
$itemNotFound = $true
ForEach ($item in $arr)
{
If ($itemNotFound)
{
If ($item -like "*`"value22`"*")
{
$itemNotFound = $false
}
}
}
if ($itemNotFound)
{
Write-Host "Not Found value22"
}
output will be:
Not Found value24