Array.Find on powershell array - powershell

How can I use the Array.Find method in powershell?
For example:
$a = 1,2,3,4,5
[Array]::Find($a, { args[0] -eq 3 })
gives
Cannot find an overload for "Find" and the argument count: "2".
At line:3 char:1
+ [Array]::Find($a, { $args[0] -eq 3 })
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
The array class has the methods I expect, as given by:
PS> [Array] | Get-Member -Static
TypeName: System.Array
Name MemberType Definition
---- ---------- ----------
Find Method static T Find[T](T[] array, System.Predicate[T] match)
Should the array be casted to something else to match the T[] type? I know there are other ways to achieve the find functionality, but I was wondering why this doesn't work.

There is no need to use Array.Find, a regular where clause would work fine:
$a = #(1,2,3,4,5)
$a | where { $_ -eq 3 }
Or this (as suggested by #mjolinor):
$a -eq 3
Or this (returns $true or $false):
$a -contains 3
Where clause supports any type of objects, not just basic types, like this:
$a | where { $_.SomeProperty -eq 3 }

You need to cast the ScriptBlock as a Predicate[T]. Consider the following example:
[Array]::Find(#(1,2,3), [Predicate[int]]{ $args[0] -eq 1 })
# Result: 1
The reason that you received the error, is because there was no matching method overload, in the case where you're passing in a PowerShell ScriptBlock. As you noted in your Get-Member output, there is no Find() method overload that accepts a ScriptBlock as its second parameter.
[Array]::Find(#(1,2,3), { $args[0] -eq 1 })
Cannot find an overload for "Find" and the argument count: "2".
At line:1 char:17
+ [Array]::Find(#(1,2,3), { $_ -eq 1 })
+ ~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest

Another option would be using an ArrayList, which provides a Contains method:
PS C:\> [Collections.ArrayList]$a = 'a', 'b', 'c'
PS C:\> $a.Contains('b')
True
PS C:\> $a.Contains('d')
False
Or, as #Neolisk mentioned in the comments, you could use PowerShell's -contains operator:
PS C:\> $a = 'a', 'b', 'c'
PS C:\> $a -contains 'b'
True
PS C:\> $a -contains 'd'
False

Trevor Sullivan's answer is the right one, not only for Find() static method, but for FindIndex() as well.
When you've got several NIC cards with both ipv4 & ipv6 active on your servers and want to check the ipv4 IP/netmask pairs, something like this is good :
$NetworkAdapters = Get-WmiObject Win32_NetworkAdapterConfiguration -Filter 'IPEnabled = True' | Select-Object -Property Description, IPAddress, IPSubnet, DefaultIPGateway, DNSServerSearchOrder, DNSDomain
$NetworkAdapters | % {
"Adapter {0} :" -f $_.Description
# array'ing to avoid failure against single homed netcards
$idx = [System.Array]::FindIndex(#($_.IPAddress), [Predicate[string]]{ $args[0] -match "\d+.\d+.\d+.\d+" })
" IP {0} has netmask {1}" -f #($_.IPAddress[$idx]), #($_.IPSubnet)[$idx]
}
My point is it works like a charm on 2012 WinPE, and fails on a production Win7 wks. Anyone got an idea ?

This was run across ~6 million items in a system.array using both methods
$s=get-date
$([array]::FindALL($OPTArray,[Predicate[string]]{ $args[0] -match '^004400702(_\d{5})?' })).count
$(New-TimeSpan -Start $s -End $(get-date)).TotalSeconds
20 items
33.2223219 seconds
$s=get-date
$($OPTArray | where { $_ -match '^004400702(_\d{5})?'}).count
$(New-TimeSpan -Start $s -End $(get-date)).TotalSeconds
20 items
102.1832173 seconds

Related

Cannot seem to identify type

Modeled on the example in Get-Help about_Type_Operators:
PS C:\> (get-culture) -is [System.Globalization.CultureInfo]
True
I am trying to do just about the same thing with a different type. Why does this fail? I copied the type name from the output of Get-TypeData.
(My apologies for the original question using is instead of -is.)
This suggestion did not work.
PS C:\> (Get-WMIObject -Class Win32_BIOS) -is [System.Management.ManagementObject#root\cimv2\Win32_BIOS]
Unable to find type [System.Management.ManagementObject#root\cimv2\Win32_BIOS].
At line:1 char:1
+ (Get-WMIObject -Class Win32_BIOS) -is [System.Management.ManagementOb ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Manageme...imv2\Win32_BIOS:TypeName)
[], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
On a related note, what is the purpose of each of these?
PS C:\> Get-TypeData | Where-Object {$_.TypeName -like '*Win32_BIOS' }
TypeName Members
-------- -------
System.Management.ManagementObject#root\cimv2\Win32_BIOS {}
Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_BIOS {}
Assuming...
PS> $bios = Get-WmiObject -Class Win32_BIOS
...you can use the __CLASS system property to test the specific WMI class of an object like this...
PS> $bios.__CLASS -eq 'Win32_BIOS'
True
...or this...
PS> $bios.SystemProperties['__CLASS'].Value -eq 'Win32_BIOS'
True
You might also test the namespace just to be really sure you've got the right class:
PS> $bios.__NAMESPACE -eq 'root\cimv2' -and $bios.__CLASS -eq 'Win32_BIOS'
True
Note that the comparisons above don't work exactly like -is because you are testing for the exact class, whereas -is takes the class hierarchy into account. That is, the following fails even though Win32_BIOS inherits from CIM_BIOSElement:
PS> $bios.__CLASS -eq 'CIM_BIOSElement'
False
The reason why $bios | Get-Member shows System.Management.ManagementObject#root\cimv2\Win32_BIOS as the type name is because Win32_BIOS and its inheritance chain have been added to the TypeNames property...
PS> $bios.PSObject.TypeNames
System.Management.ManagementObject#root\cimv2\Win32_BIOS
System.Management.ManagementObject#root\cimv2\CIM_BIOSElement
System.Management.ManagementObject#root\cimv2\CIM_SoftwareElement
System.Management.ManagementObject#root\cimv2\CIM_LogicalElement
System.Management.ManagementObject#root\cimv2\CIM_ManagedSystemElement
System.Management.ManagementObject#Win32_BIOS
System.Management.ManagementObject#CIM_BIOSElement
System.Management.ManagementObject#CIM_SoftwareElement
System.Management.ManagementObject#CIM_LogicalElement
System.Management.ManagementObject#CIM_ManagedSystemElement
System.Management.ManagementObject
System.Management.ManagementBaseObject
System.ComponentModel.Component
System.MarshalByRefObject
System.Object
The actual type is still ManagementObject...
PS> $bios.GetType().FullName
System.Management.ManagementObject
You are using the string is as the comparison operator; however, all comparison operators begin with the hyphen, so you should be using -is: (Get-WMIObject -Class Win32_BIOS) -is [System.Management.ManagementObject...]

Why does this filter fail to accept certain $null values from the pipeline? [duplicate]

This question already has answers here:
Why and how are these two $null values different?
(3 answers)
Closed 6 years ago.
Context
Consider the following helper function:
Filter If-Null(
[Parameter(ValueFromPipeline=$true)]$value,
[Parameter(Position=0)]$default
) {
Write-Verbose "If ($value) {$value} Else {$default}"
if ($value) {$value} else {$default}
}
It's basically a null-coalescing operator implemented as a pipeline function. It's supposed to work like so:
PS> $myVar = $null
PS> $myVar | If-Null "myDefault" -Verbose
VERBOSE: If () {} Else {myDefault}
myDefault
However, when I set $myVar to the first element in an empty array...
PS> $myVar = #() | Select-Object -First 1
...which should effectively be the same as $null...
PS> $myVar -eq $null
True
PS> -not $myVar
True
...then the piping does not work anymore:
PS> $myVar | If-Null "myDefault" -Verbose
There is not output at all. Not even the verbose print. Which means If-Null is not even executed.
The Question
So it seems like #() | select -f 1, although being -eq to $null, is a somewhat different $null that somehow breaks piping?
Can anyone explain this behaviour? What am I missing?
Additional Information
PS> (#() | select -f 1).GetType()
You cannot call a method on a null-valued expression.
At line:1 char:1
+ (#() | select -f 1).GetType()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
PS> (#() | select -f 1) | Get-Member
Get-Member : You must specify an object for the Get-Member cmdlet.
At line:1 char:23
+ (#() | select -f 1) | Get-Member
+ ~~~~~~~~~~
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
PS> $PSVersionTable
Name Value
---- -----
PSVersion 5.0.10586.117
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.10586.117
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
Solution
Ansgar's explanation is correct (a better explanation can be found in mklement0's answer to the duplicate question). I just wanted to share my solution to the problem.
I fixed If-Null such that it returns the $default even when nothing is processed:
Function If-Null(
[Parameter(ValueFromPipeline = $true)]$value,
[Parameter(Position = 0)]$default
) {
Process {
$processedSomething = $true
If ($value) { $value } Else { $default }
}
# This makes sure the $default is returned even when the input was an empty array or of
# type [System.Management.Automation.Internal.AutomationNull]::Value (which prevents
# execution of the Process block).
End { If (-not $processedSomething) { $default }}
}
This version does now correctly handle empty pipeline results:
PS> #() | select -f 1 | If-Null myDefault
myDefault
Arrays are unrolled by pipelines, so that each array element is passed on separately. If you pass an empty array into a pipeline it essentially is unrolled into nothing, meaning that the downstream cmdlet is never invoked, thus leaving you with an empty variable.
You can observe this behavior by passing $null and #() into a loop that just echoes a string for each input item:
PS C:\> #() | % { 'foo' } # no output here!
PS C:\> $null | % { 'foo' } # output: foo
foo
Depending on the context this is different from a variable with the "value" $null. Even though in most cases PowerShell will automatically convert the "empty" variable to a value of $null (as seen in your checks) it doesn't do so when passing the variable into the pipeline. In that case you still don't pass anything into the pipeline, hence your filter is never invoked.

Not able to parse multiline array in powershell

My aws cli returns multiline array with nested hash, I am not able to parse output to get appropriate values from it
>Write-Host $a
[
[
[
{
"VolumeID": "vol-fxxxxxx"
}
]
]
]
If I do $a[0], it returns first line i.e. "[" same with incremental indexs.
How I can parse this array and get the volumeID from it ?
Thanks
Additional details :
Thanks for your valuable answers. Here is the class name of the above object :
> $a.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
> $a | Where-Object ($_ -like "*Volume*")
> $a | ConvertFrom-Json
ConvertFrom-Json : Invalid JSON primitive:
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData.
At line:1 char:6
+ $a | ConvertFrom-Json
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [ConvertFrom-Json], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand
Hi Martin Brandl, TessellatingHeckler I have updated with your question, could you please help me out on the same.
Thanks
same with incremental indexs
I can't make sense of incremental indexes doing the same, and returning the first line, but it looks like it's either:
A multiline string ($a.gettype() has Name: String):
$b = $a | ConvertFrom-Json
$b[0][0][0].VolumeID
or, an array of string ($a.gettype() Name is Object[] or String[]), do this first, then convert from JSON:
$a = -join $a
You could use the where-object cmdlet.
$a | where-object {$_ -like "*Volume*"}
More information on the where-object cmdlet can be found here: https://technet.microsoft.com/en-us/library/ee177028.aspx
Thanks, Tim.

Get hashtable value by key of group object under strict mode version 2?

The following script, which pivot the array list by x and y, doesn't work. ($hashvariable.x not working). How to rewrite it? It seems it's not easy to simple get a value by key in a hashtable under strict mode.
Set-StrictMode -version 2 # change 2 to 1 will work
$a = #('a','b','x',10),
#('a','b','y',20),
#('c','e','x',50),
#('c','e','y',30)
$a | %{
new-object PsObject -prop #{"label" = "'$($_[0])','$($_[1])'"; value=#{ $_[2]=$_[3]}}
} |
group label | % {
"$($_.Name), $($_.Group.value.x), $($_.Group.value.y)" # error
#"$($_.Name), $($_.Group.value['x']), $($_.Group.value['y'])" # empty for x,y
}
Expected result.
'a','b', 10, 20
'c','e', 50, 30
Error:
Property 'x' cannot be found on this object. Make sure that it exists.
At line:6 char:35
+ "[$(#($_.Name -split ",") + #($_.Group.value.x, $_.Group.value.y))]"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
+ FullyQualifiedErrorId : PropertyNotFoundStrict
Not sure what you really want, but this is my best guess. You should accumulate all objects in one group in single Hashtable object instead of creating separate Hashtable for each input object:
$a = ('a','b','x',10),
('a','b','y',20),
('c','e','x',50),
('c','e','y',30)
$a |
Group-Object {$_[0]},{$_[1]} |
Select-Object Values,
#{
Name='Group'
Expression={
$_.Group |
ForEach-Object {$t=#{}}{$t.Add($_[2],$_[3])}{$t}
}
} |
ForEach-Object {
'''{0}'',''{1}'', {2}, {3}'-f#($_.Values;$_.Group['x','y'])
}
It is difficult to understand what, in fact, this script does, anyway, the problem is that only first element in a collection $_.Group.Value has the property 'x', at the same time, strict mode prohibits references to non-existent properties of an object, so you receive this error.
Here is a solution, hope I've understood right what you want:
$a = #('a','b','x',10),
#('a','b','y',20),
#('c','e','x',50),
#('c','e','y',30)
$hashtable = #{}
$a |%{
$hashtable["$($_[0]) $($_[1])"] +=
[hashtable]#{$_[2] = $_[3]}
}
$hashtable.GetEnumerator() | %{
"$($_.Key) $($_.Value['x']) $($_.Value['y'])"
}

Why don't errors get returned when calling properties that don't exist?

Given the following snippet
$drives = Get-PSDrive
foreach($drive in $drives)
{
Write-Host $drive.Name "`t" $drive.Root
Write-Host " - " $drive.Free "`t" $drive.PropertyDoesntExist
}
The drive.PropertyDoesntExist property doesn't... erm... exist so I would expect an error to be thrown but instead it returns a null.
How can I get errors or exceptions?
EDIT - Me bad - I asked 2 questions in one so I moved one into a separate question.
The NextHop Blog provides a good solution to this problem. It doesn't give you an error, but instead a boolean. You can use Get-Member to get a collection of all of the real properties of the object's type and then match for your desired property.
Here's an example for strings:
PS C:\> $test = "I'm a string."
PS C:\> ($test | Get-Member | Select-Object -ExpandProperty Name) -contains "Trim"
True
PS C:\> ($test | Get-Member | Select-Object -ExpandProperty Name) -contains "Pigs"
False
If you explicitly want an error, you may want to look into Set-Strictmode as Set-StrictMode -version 2 to trap non-existent properties. You can easily turn it off when you're done with it, too:
PS C:\> Set-StrictMode -version 2
PS C:\> "test".Pigs
Property 'Pigs' cannot be found on this object. Make sure that it exists.
At line:1 char:8
+ "test". <<<< Pigs
+ CategoryInfo : InvalidOperation: (.:OperatorToken) [], RuntimeException
+ FullyQualifiedErrorId : PropertyNotFoundStrict
PS C:\> Set-StrictMode -off