Powershell: null file always generated (output of Compare-Object)
Asked Answered
A

2

2

The most popular answer for this question involves the following Windows powershell code (edited to fix a bug):

$file1 = Get-Content C:\temp\file1.txt  
$file2 = Get-Content C:\temp\file2.txt   
$Diff = Compare-Object $File1 $File2  
$LeftSide = ($Diff | Where-Object {$_.SideIndicator -eq '<='}).InputObject  
$LeftSide | Set-Content C:\temp\file3.txt

I always get a zero byte file as the output, even if I remove the $Diff line.

Why is the output file always null, and how can it be fixed?

Abreu answered 17/6, 2017 at 16:49 Comment(6)
Are we expected to know, why you'd expect anything else, or what that would be?Reaganreagen
Regardless of the input files, the output is always null.Abreu
I get content in file3.txt using your sample.Tote
@Tote Thanks. Could it be a Powershell version issue? I need to support v2.Abreu
It could be! That's pretty old now, I don't see any obvious problems, but if I had to guess it the next-to-last line that looks fragile.Tote
Member enumeration is v3+ feature. Thus ($Diff | Where-Object {$_.SideIndicator -eq '<='}).InputObject will only work when $Diff | Where-Object {$_.SideIndicator -eq '<='} produce exactly one item.Yogh
F
6

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.

Fungous answered 18/6, 2017 at 22:37 Comment(1)
You should make a powershell dictionary (I say powershell since some meanings, mean different things in some programming languages). Definitely would be a hot seller lots of us would advertise and buy! I struggle with lots of this stuff and the MS documentation can come off a bit confusing, and your explanations make it a lot easier to understand. 10/10 would buy! @Santiago Squarzon, back me up on this... lolPorpoise
T
1

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 } 
Tote answered 18/6, 2017 at 2:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.