Why does select-object -first 1 not return an array for consistency? - powershell

When I pipe some objects to select-object -first n it returns an array except if n is 1:
PS C:\> (get-process | select-object -first 1).GetType().FullName
System.Diagnostics.Process
PS C:\> (get-process | select-object -first 2).GetType().FullName
System.Object[]
For consistency reasons, I'd have expected both pipelines to return an array.
Apparently, PowerShell chooses to return one object as object rather than as an element in an array.
Why is that?

Why questions are generally indeterminate in cases like this, but it mostly boils down to:
Since we asked for the "-first 1" we would expect a single item.
If we received an array/list we would still need to index the first one to obtain just that one, which is pretty much what "Select-Object -First 1" is designed to do (in that case.)
The result can always be wrapped in #() to force an array -- perhaps in the case where we've calculated "-First $N" and don't actually know (at that moment in the code) that we might receive only 1.
The designer/developer thought it should be that way.
It's #3 that keeps it from being an issue:
$PSProcess = #(Get-Process PowerShell | Select -First 1)
...this will guarantee $PSProcces is an array no matter what the count.
It even works with:
$n = Get-Random 3
#(Get-Process -first $n) # $n => 0, 1, or 2 but always returns an array.

The pipeline will return the [System.Diagnostics.Process] object. In your first example it's only one object. The second one is an [System.Object[]] array of the [System.Diagnostics.Process].
$a = (get-process | select-object -first 1)
$a | Get-Member
$b = (get-process | select-object -first 2)
,$b | Get-Member

Related

How do I limit the results to 20?

Need to the limit the results of this to the first 20.
Is there another way I can loop this to get 20 results at a time?
$Array = #()
#Fill $Array with Data
$List = $Array | ForEach-Object {"$_`n"}
Personally, I would limit ther returned results prior to the looping construct. My example uses the names of service objects, but I needed something to use...
$TOlist = (Get-Service).Name
$TOlist | Select-Object -First 20 | ForEach-Object {
$_
}
You'll need to use at least two lines here. As far as I'm aware, you can't both assign your $TOlist variable and start sending each value down the pipeline.

Count properties of an object

I try to count the number of drives on certain VM's in a Cluster:
(Get-ClusterGroup -Cluster <Name> |
Where-Object {$_.GroupType –eq 'VirtualMachine'} |
Get-VM |
Measure-Object -Property Harddrives).Count
--> Returns 55, the count of VM's in the Cluster
Several VM's have more than one Harddrive, how can I retrieve the proper Count of Drives in a pipelined command?
Try enumerating the property:
$harddrives = Get-ClusterGroup -Cluster '<String>' | ? GroupType -eq VirtualMachine |
Get-VM | % HardDrives
$harddrives.Count
Some shorthand in v4+:
(#(Get-ClusterGroup -Cluster '<String>').
Where({ $_.GroupType -eq 'VirtualMachine' }) |
Get-VM).HardDrives.Count
I had a problem that was closer to the original question. I had a custom PowerShell object that I needed to literally count how many properties were in it. I found this:
($Object | Get-Member -MemberType NoteProperty | Measure-Object).Count
To complement TheIncorrigible1's helpful answer, which contains an effective solution but only hints at the problem with your Measure-Object call:
Perhaps surprisingly, Measure-Object doesn't enumerate input objects or properties that are themselves collections, as the following examples demonstrate:
PS> ((1, 2), (3, 4) | Measure-Object).Count
2 # !! The input arrays each counted as *1* object - their elements weren't counted.
PS> ([pscustomobject] #{ prop = 1, 2 }, [pscustomobject] #{ prop = 3, 4 } |
Measure-Object -Property prop).Count
2 # !! The arrays stored in .prop each counted as *1* object - their elements weren't counted.
The above applies as of Windows PowerShell v5.1 / PowerShell Core v6.1.0.
This GitHub issue suggests introducing a -Recurse switch that would allow opting into enumerating collection-valued input objects / input-object properties.

How to strip out everything but numbers in a variable?

I'm trying to automate setting an IP address through PowerShell and I need to find out what my interfaceindex number is.
What I have worked out is this:
$x = ( Get-NetAdapter |
Select-Object -Property InterfceName,InterfaceIndex |
Select-Object -First 1 |
Select-Object -Property Interfaceindex ) | Out-String
This will output:
InterfaceIndex
--------------
3
Now the problem, when I try to grab only the number using:
$x.Trim.( '[^0-9]' )
it still leave the "InterfaceIndex" and the underscores. This causes the next part of my script to error because I just need the number.
Any suggestions?
This will get your job done:
( Get-NetAdapter | Select-Object -Property InterfceName,InterfaceIndex | Select-Object -First 1 | Select-Object -Property Interfaceindex).Interfaceindex
Actually you do not need two times to select the property: do like this:
( Get-NetAdapter |Select-Object -First 1| Select-Object -Property InterfceName,InterfaceIndex).Interfaceindex
Answering your immediate question: you can remove everything that isn't a number from a variable by, well, removing everything that isn't a number (or rather digit):
$x = $x -replace '\D'
However, a better aproach would be to simply not add what you want removed in the first place:
$x = Get-NetAdapter | Select-Object -First 1 -Expand InterfaceIndex
PowerShell cmdlets usually produce objects as output, so instead of mangling these objects into string form and cutting away excess material you normally just expand the value of the particular property you're interested in.
(Get-NetAdapter | select -f 1).Interfaceindex
No point in selecting properties as they are there by default. If you want to keep object do:
(Get-NetAdapter | select -f 1 -ov 'variablename').Interfaceindex
where f = first, ov = outvariable
$variablename.Interfaceindex
You don't need Out-String as cast to string is implicit when you output to screen. and if you try to work with this data further down powershell is clever enough to cast it from int to string and vice versa when needed.

Handle output that can either be an array or a single string

I have script which retrieves data from a web service in XML format. Certain elements can be there - or not - and can contain one or more sub elements. Therfore I retrieve the value with this:
$uid = $changeRecord.newAttrs | Where{$_.name -eq 'uid'} | Select -ExpandProperty Values | select -Index 0
This works fine. Mostly there is only one sub element in the <values> part of the answer and even if not, I am only interested in the first one. However, the last part | select -Index 0 produces silent warnings into the Windows Event Log (see also here ) if there is only one element within <values>. Therefore I would like to get rid of the error.
So I am looking for a way to achieve the same behaviour without it throwings errors - and possible not just put try-catch around.
Thanks!
// Update: As discussed below, the answers presented so far do not solve the issue. The closest by now is
([array]($changeRecord.newAttrs | Where{$_.name -eq 'uid'} | Select -ExpandProperty Values))[0]
This, however, fails with an error if the array does not contain any elements. Any idea if this can be handled as well within one line?
Have you tried this: Select-Object -First 1 ?
$uid = $changeRecord.newAttrs |
Where-Object {$_.name -eq 'uid'} |
Select-Object -ExpandProperty Values |
Select-Object -First 1
Select -Index n is only meant to be used on arrays as it will explicitly select from that index in the array. Therefore you will have issues when doing it on a single object. Select -First n will get you n number of objects off the pipeline.
All that said, when I am calling a command and the results may either be a single item or an array of items, I generally declare the variable as an array or cast the value as an array and then even if I get a single object back from the command it will be stored in an array. That way no matter what gets returned, I am treating it the same way. So in your case:
$uid = [array]($changeRecord.newAttrs | Where{$_.name -eq 'uid'} | Select -ExpandProperty Values) | select -Index 0
So, I finally found a solution which is probably fine for me:
$valueArray = [array]($changeRecord.newAttrs | Where{$_.name -eq 'uid'} | Select -ExpandProperty Values)
if(($valueArray -ne $null) -and ($valueArray.Count -gt 0))
{
$value = $valueArray.GetValue(0)
}
else
{
$value = "null..."
}
I put the whole thing into an array first and then check if the array contains any elements. Only if so, I get the first value.
Thanks for everybodys help!

Return object from array with highest value

I want to return an object from an array who's property has the highest value. Currently I am doing the following
Get-VM | Sort-Object -Property ProvisionedSpaceGB | Select-Object -Last 1
This works but is inefficient. I don't need the entire array sorted, I just need the object with largest value. Ideally I would use something like
Get-VM | Measure-Object -Property ProvisionedSpaceGB -Maximum
but this only returns the value of the object property, not the entire object. Is there a way to have measure-object return the base object?
Not directly. Measure-Object is intended to be an easy way to grab such values, not their input objects. You could get the maximum from Measure-Object and then compare against the array, but it takes a few steps:
$array = Get-VM
$max = ($array | measure-object -Property ProvisionedSpaceGB -maximum).maximum
$array | ? { $_.ProvisionedSpaceGB -eq $max}
You could also forgo Measure-Object entirely and iterate through the set, replacing the maximum and output as you go.
$max = 0
$array | Foreach-Object
{
if($max -le $_.ProvisionedSpaceGB)
{
$output = $_
$max = $_.ProvisionedSpaceGB
}
}
$output
This is a little dirtier so as to always return a single value. It would need a minor adjustment if you were to reuse it in a case where there may be multiple values that have the same maximum (filesize lengths when using Get-ChildItem, for example). It will replace $output with the latter iterate in a case where two or more objects have the same value for ProvisionedSpaceGB. You could turn $output into a collection easily enough to fix that.
I prefer the former solution myself, but I wanted to offer a different way to think about the problem.
You can use this:
$array = Get-VM | Sort-Object -Property ProvisionedSpaceGB -Descending
$array[0]