How to check if PowerShell result contains these words - powershell

I'm doing an IF statement in PowerShell and at some point I do this:
(Get-BitlockerVolume -MountPoint "C:").KeyProtector.keyprotectortype
which gives me the results in this format, on top of each other
I want to write my IF statement to check whether the output of the command above contains both "TpmPin" and "RecoveryPassword" but not sure what the correct syntax is.
I tried something like this but it doesn't work as expected, the result is always true even if it should be false.
if ((Get-BitlockerVolume -MountPoint "C:").KeyProtector.keyprotectortype -contains "tpmpin" && "RecoveryPassword")
this doesn't work either:
if ((Get-BitlockerVolume -MountPoint "C:").KeyProtector.keyprotectortype -contains "tpmpinRecoveryPassword")
p.s I don't want to do nested IF statements because I'm already doing multiple of them.

Make the call to Get-BitLockerVolume before the if statement, store the result in a variable, then use the -and operator to ensure both are found:
$KeyProtectors = Get-BitlockerVolume -MountPoint "C:" |ForEach-Object KeyProtector
if($KeyProtectors.KeyProtectorType -contains 'TpmPin' -and $KeyProtectors.KeyProtectorType -contains 'RecoveryPassword'){
# ... both types were present
}
If you have an arbitrary number of values you want to test the presence of, another way to approach this is to test that none of them are absent:
$KeyProtectors = Get-BitlockerVolume -MountPoint "C:" |ForEach-Object KeyProtector
$mustBePresent = #('TpmPin', 'RecoveryPassword')
if($mustBePresent.Where({$KeyProtectors.KeyProtectorType -notcontains $_}, 'First').Count -eq 0){
# ... all types were present
}

you can write a powershell if check like given below:
if((((Get-BitlockerVolume -MountPoint "C:").KeyProtector.keyprotectortype) -join ",") -eq "Tpm,RecoveryPassword")
{
write-host "matches"
}
else
{
write-host "does not match"
}
matches
Caveat: As told by #MathiasR.Jessen, this solution assumes the order of the values. So, if the values order changes, above solution will not work. We need to follow the solution provided by #MathiasR.Jessen

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

Return boolean from string search

I'm trying to return TRUE from searching Get-ComplianceSearch's output for 'Completed'. My code below is a simple wait loop. But I don't think I'm returning the value correctly because the loop never finishes. I'm fairly new to PowerShell. Please assist or direct.
I'm using Powershell Core 7.1. There are no errors but the Search-String condition never returns TRUE.
try {
$timer = [Diagnostics.Stopwatch]::StartNew()
while (($timer.Elapsed.TotalSeconds -lt $Timeout) -and (-not (Get-ComplianceSearch -
Identity $searchName | Select-String 'Completed' -SimpleMatch -Quiet))) {
Start-Sleep -Seconds $RetryInterval
$totalSecs = [math]::Round($timer.Elapsed.TotalSeconds, 0)
Write-Verbose -Message "Still waiting for action to complete after [$totalSecs]
seconds..."
}
$timer.Stop()
if ($timer.Elapsed.TotalSeconds -gt $Timeout) {
throw 'Action did not complete before timeout period.'
} else {
Write-Verbose -Message 'Action completed before timeout period.'
}
} catch {
Write-Error -Message $_.Exception.Message
}
(This is the expected output of the command Get-ComplianceSearch)
Okay, you don't want to use Select-String here (although you can, see #mklement0's helpful answer, looking at object properties is usually preferred). That is returning an object and you want to check the Status property for "Completed". Make the following change to the -not subexpression:
(-not (Get-ComplianceSearch -Identity $searchName | Where-Object {
$_.Status -eq 'Completed'
}))
The above can be on one line but I broke it up for readability.
Basically, Select-String looks for content in strings. If you are looking for a particular value of an object property however, you can use Where-Object to test for a condition and return any objects matching that condition. In this case, we want to return any object that have a Status of 'Completed', so we can negate that in the if statement.
You (or others) might be wondering how this works since Where-Object returns matching objects, but not booleans. The answer is "truthiness". PowerShell objects are "truthy", which means anything can be evaluated as a [bool].
The following values evaluate to $false in most cases. I've included some gotchas to watch out for when relying on "truthy" values:
A numeric value of 0
A string value of 0 evaluates as $true
Empty arrays
Empty strings
A whitespace-only string or strings consisting only of non-printable characters evaluates as $true
$false
A string value of False evaluates as $true
Most everything else will evaluate to $true. This is also why comparison operators are syntactically optional when checking whether a variable is $null or not. Although there are times when an explicit value check is a good idea as comparison operators compare the actual values instead of only whether the variable "is" or "isn't".
How does this apply to the expression above then? Simple. if statements, always treat the condition expression as a [bool], no conversion required. In addition, logical operators and conditional operators also imply a boolean comparison. For example, $var = $obj assigns $obj to $var, but$var = $obj -eq $obj2 or $var = $obj -and $obj2 will assign $true or $false.
So knowing the above, if Where-Object returns nothing, it's $false. If it returns a tangible object, it's $true.
Bender the Greatest's helpful answer shows a better alternative to using Select-String, because OO-based filtering that queries specific properties is always more robust than searching string representations.
That said, for quick-and-dirty interactive searches, being able to search through a command's formatted display output can be handy, and, unfortunately, Select-String does not do that by default.
As for what you tried:
To make your Select-String work, you need to insert Out-String -Stream before the Select-String call, so as to ensure that the for-display representation is sent through the pipeline, line by line.
# `oss` can be used in lieu of `Out-String -Stream` in PSv5+.
# `sls` can be used in lieu of `Select-String`.
Get-ComplianceSearch | Out-String -Stream | Select-String 'Completed' -SimpleMatch -Quiet
Note:
If you want to search a for-display representation other than the default one, you can insert a Format-* cmdlet call before the Out-String -Stream segment; e.g.
Get-Item / | Format-List * | Out-String -Stream | Select-String ... would search through a list representation of all properties of the object output by Get-Item.
Perhaps surprisingly, Select-String does not search an input object's for-display representation, as you would see it in the console, using the rich formatting provided by PowerShell's display-formatting system.
Instead, it performs simple .ToString() stringification, whose results are often unhelpful and cannot be relied upon to include the values of properties. (E.g.,
#{ foo = 'bar' } | Select-String foo does not work as intended; it is equivalent to
#{ foo = 'bar' }.ToString() | Select-String foo and therefore to
'System.Collections.Hashtable' | Select-String foo
Arguably, Select-String should always have defaulted to searching through the input objects' formatted string representations:
That there is demand for this behavior is evidenced by the fact that PowerShell versions 5 and above (both editions) ship with the oss convenience function, which is a wrapper for Out-String -Stream.
GitHub issue #10726 asks that the current behavior of Select-String be changed to search the for-display string representations by default.

PowerShell return multiple values from if condition

I have a Powershell script returning data from an API which works fine as long as I only attempt to return one $device.realm, but I need multiple realms. I'm a newb to PS.
Any help here is really appreciated
Here is my code
$Output = forEach ($device in $devices) {
if ($device.realmName -eq 'Archive') {
[PSCustomObject]#{
HostName = $device.name
IPAddress = $device.primaryInterfaceAddress
Realm = $device.realmName
SerialNumbers = (($device.dynamicFields | where { $_.name -EQ "serial number" } | Select-Object -ExpandProperty values) -join "," | out-string).TrimEnd()
}| Select-Object Hostname,IPAddress,Realm,SerialNumbers | Export-csv C:\temp\Archive.csv -notype -Append
}
I need to return multiple $device.realms as in
if ($device.realmName -eq 'Archive' -and 'Default' -and 'Farms')
Once I add the additional -and's every realm is returned instead of just the one's I need to return.
I believe the issue at hand here is that the statement within the If block that you're querying as ($device.realmName -eq 'Archive' -and 'Default' -and 'Farms')
is not, when evaluated logically "Evaluate true if the device realmname is Archive, Default, or Farms." It is evaluating whether device.realmname is archive, and then just interpreting the two -ands in your example as true, as they are not querying a comparison, but just the presence of a non-null string. Not sure what is leading it to return everything, I'd have to see some more direct examples to be sure, but in my experience that is most common when you include an -or in a comparison pointing to a nonnull string, which will make the entire statement true.
What I would suggest is as follows: Use the regex operators built in to powershell for cases like this. You could use
if($device.realmname -eq 'Archive' -or $Device.realmname -eq 'farm' -or $device.realmname -eq 'Default')
which would, I believe, return what you are looking for, but I find it a bit complex. More complicated queries on a single property, I find, are easiest to do via -match, through something invoking the -match operator, which allows you to build a regex query statement that can include Or's or And's with a bit simpler of a synatax, like so:
if($Device.realmName -match 'Archive|Farm|Default')

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

Writing $null to Powershell Output Stream

There are powershell cmdlets in our project for finding data in a database. If no data is found, the cmdlets write out a $null to the output stream as follows:
Write-Output $null
Or, more accurately since the cmdlets are implemented in C#:
WriteOutput(null)
I have found that this causes some behavior that is very counter to the conventions employed elsewhere, including in the built-in cmdlets.
Are there any guidelines/rules, especially from Microsoft, that talk about this? I need help better explaining why this is a bad idea, or to be convinced that writing $null to the output stream is an okay practice. Here is some detail about the resulting behaviors that I see:
If the results are piped into another cmdlet, that cmdlet executes despite no results being found and the pipeline variable ($_) is $null. This means that I have to add checks for $null.
Find-DbRecord -Id 3 | For-Each { if ($_ -ne $null) { <do something with $_> }}
Similarly, If I want to get the array of records found, ensuring that it is an array, I might do the following:
$recsFound = #(Find-DbRecord -Category XYZ)
foreach ($record in $recsFound)
{
$record.Name = "Something New"
$record.Update()
}
The convention I have seen, this should work without issue. If no records are found, the foreach loop wouldn't execute. Since the Find cmdlet is writing null to the output, the $recsFound variable is set to an array with one item that is $null. Now I would need to check each item in the array for $null which clutters my code.
$null is not void. If you don't want null values in your pipeline, either don't write null values to the pipeline in the first place, or remove them from the pipeline with a filter like this:
... | Where-Object { $_ -ne $null } | ...
Depending on what you want to allow through the filter you could simplify it to this:
... | Where-Object { $_ } | ...
or (using the ? alias for Where-Object) to this:
... | ? { $_ } | ...
which would remove all values that PowerShell interprets as $false ($null, 0, empty string, empty array, etc.).