Subhayan Bhattacharya
Subhayan Bhattacharya

Reputation: 5715

Difference between select-object and using foreach on the same object

Can someone please help me understand the difference between the below two pieces of code . Why are the results different for both . I each case i am selecting the same property (name) :

Code 1 :

$obj = Get-Service | Where-Object {$_.Status -eq "Running"} | foreach-object  {$_.Name} | select -first 3
foreach ( $item in $obj ) { write-output "Name is : $item" }
Output :
Name is : AeLookupSvc
Name is : Appinfo
Name is : AudioEndpointBuilder

Code 2 :

$obj = Get-Service | Where-Object {$_.Status -eq "Running"} | select -first 3 name
foreach ( $item in $obj ) { write-output "Name is : $item" }
Output :
Name is : @{Name=AeLookupSvc}
Name is : @{Name=Appinfo}
Name is : @{Name=AudioEndpointBuilder}

Upvotes: 11

Views: 16751

Answers (3)

JohnLBevan
JohnLBevan

Reputation: 24430

Select-Object returns an array of custom objects to the pipeline; in this case having only one property which happens to be a string.
As @walidtourni mentions, using expand works around this issue. This is because expand causes the output to be the property's value, instead of a custom object with a property with that value. The reason this is possible is expand only takes one argument; i.e. there's no possibility of you attempting to return multiple values for the same "row".

The foreach-object on the other hand is simply spitting stuff out to the pipeline. If you tried to include a second property without manually wrapping both into a custom object, the output would create another row rather than two properties on the same row.

To demonstrate, run the following:

Clear
$x = Get-Service | Where-Object {$_.Status -eq "Running"}  | select -first 3 
$x | foreach-object  {$_.Name}  #returns 3 rows, single column; string
$x | foreach-object  {$_.Name;$_.CanPauseAndContinue;} #returns 6 rows, single column; alternate string & boolean
$x | select Name #returns 3 rows, single column (string); custom object
$x | select Name, CanPauseAndContinue #returns 3 rows, two columns (string & boolean); custom property
$x | select -expand Name #returns 3 rows, single column; string; notice the lack of column header showing this is a string, not a string property of a custom object
$x | select -expand Name,CanPauseAndContinue #error; -expand can only take single valued arguments
$x | select -expand Name -expand CanPauseAndContinue #error; you can't try to define the same paramenter twice

Upvotes: 14

carrvo
carrvo

Reputation: 638

This is an old post but I thought I would expand on @JohnLBevan's answer.

Select-Object's normal operation is to create a PSCustomObject that copies over the properties from the original object.

Get-Process
Get-Process | Select-Object Name,CPU

If you only specify one property, then it creates an object with only one property (Get-Process | Select-Object Name)--which is what you are seeing with the @{Name=AeLookupSvc}, et cetera. Note that this is strictly different than the value of the property, which it has a parameter to support getting:

Get-Process | Select-Object -ExpandProperty Name

ForEach-Object, on the other hand, acts more like a loop body executing against each pipeline object. This is easily used for retrieving the value of a property to be passed along, and even comes with a convenient shorthand for doing so.

Get-Process | ForEach-Object Name

So to really add on to the other answers, I did a bit of exploration as to the performance difference:

# Benchmark
1..100 | ForEach-Object {Measure-Command {Get-WinEvent -LogName System -ErrorAction SilentlyContinue} | Select-Object -ExpandProperty TotalMinutes} | Measure-Object -Average
# Average: 2.01325857505 Minutes

# Select-Object -ExpandProperty
1..100 | ForEach-Object {Measure-Command {Get-WinEvent -LogName System -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LevelDisplayName -ErrorAction SilentlyContinue} | Select-Object -ExpandProperty TotalMinutes} | Measure-Object -Average
# Average: 2.87394915385 Minutes
# note the `Select-Object -ErrorAction SilentlyContinue` because not all pipeline records are accepted by Select-Object (strange, but I did not explore why)

# ForEach-Object
1..100 | ForEach-Object {Measure-Command {Get-WinEvent -LogName System -ErrorAction SilentlyContinue | ForEach-Object LevelDisplayName} | Select-Object -ExpandProperty TotalMinutes} | Measure-Object -Average
# Average: 3.10188571238333 Minutes

Note that I ran these in parallel (different PowerShell windows) on PowerShell 5.1.

Upvotes: 2

walid toumi
walid toumi

Reputation: 2272

For similar result you can change the second example like this:

select -first 3 -expand name

Select-object select properties object

Upvotes: 3

Related Questions