Powershell: null file always generated (output of Compare-Object)
PetSerAl, as he routinely does, has provided the crucial pointer in a comment on the question:
Member enumeration - the ability to access a member (a property or a method) on a collection and have it implicitly applied to each of its elements, with the results getting collected in an array, was introduced in PSv3.[1]
Member enumeration is not only expressive and convenient, it is also faster than alternative approaches.
A simplified example:
PS> ((Get-Item /), (Get-Item $HOME)).Mode
d--hs- # The value of (Get-Item /).Mode
d----- # The value of (Get-Item $HOME).Mode
Applying .Mode
to the collection that the (...)
-enclosed command outputs causes the .Mode
property to be accessed on each item in the collection, with the resulting values returned as an array (a regular PowerShell array, of type[System.Object[]]
).
Caveats: Member enumeration handles the resulting array like the pipeline does, which means:
-
If the array has only a single element, that element's property value is returned directly, not inside a single-element array:
PS> @([pscustomobject] @{foo=1}).foo.GetType().Name Int32 # 1 was returned as a scalar, not as a single-element array.
-
If the property values being collected are themselves arrays, a flat array of values is returned:
PS> @([pscustomobject] @{foo=1,2}, [pscustomobject] @{foo=3,4}).foo.Count 4 # a single, flat array was returned: 1, 2, 3, 4
Also, member enumeration only works for getting (reading) property values, not for setting (writing) them.
This asymmetry is by design, to avoid potentially unwanted bulk modification; in PSv4+, use .ForEach('<property-name', <new-value>)
as the quickest workaround (see below).
This convenient feature is NOT available, however:
- if you're running on PSv2 (categorically)
- if the collection itself has a member by the specified name, in which case the collection-level member is applied.
For instance, even in PSv3+ the following does NOT perform member enumeration:
PS> ('abc', 'cdefg').Length # Try to report the string lengths
2 # !! The *array's* .Length property value (item count) is reported, not the items'
In such cases - and in PSv2 in general - a different approach is needed:
-
Fastest alternative, using the
foreach
statement, assuming that the entire collection fits into memory as a whole (which is implied when using member enumeration).
PS> foreach ($s in 'abc', 'cdefg') { $s.Length }
3
5
-
PSv4+ alternative, using collection method
.ForEach()
, also operating on the collection as a whole:
PS> ('abc', 'cdefg').ForEach('Length')
3
5
Note: If applicable to the input collection, you can also set property values with .ForEach('<prop-name>', <new-value>)
, which is the fastest workaround to not being able to use .<prop-name> = <new-value>
, i.e. the inability to set property values with member enumeration.
- Slowest, but memory-efficient approaches, using the pipeline:
Note: Use of the pipeline is only memory-efficient if you process the items one by one, in isolation, without collecting the results in memory as well.
Using the ForEach-Object
cmdlet, as in Burt Harris' helpful answer:
PS> 'abc', 'cdefg' | ForEach-Object { $_.Length }
3
5
For properties only (as opposed to methods), Select-Object -ExpandProperty
is an option; it is conceptually clear and simple, and virtually on par with the ForEach-Object
approach in terms of performance (for a performance comparison, see the last section of this answer):
PS> 'abc', 'cdefg' | Select-Object -ExpandProperty Length
3
5
[1] As of this writing, the feature is described, but not named in the conceptual about_Properties
help topic. GitHub docs issue #8437 asks for it to be given an official name.
Perhaps instead of
$LeftSide = ($Diff | Where-Object {$_.SideIndicator -eq '<='}).InputObject
PowerShell 2 might work better with:
$LeftSide = $Diff | Where-Object {$_.SideIndicator -eq '<='} |
Foreach-object { $_.InputObject }