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-access 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-access 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-access 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-access 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-access 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-access 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-access 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] Previously, the feature was semi-officially known as just member enumeration, introduced in this 2012 blog post along with the feature itself. A decision to formally introduce the term member-access enumeration was made in early 2022.

Leave a Comment