Powershell: null file always generated (output of

2020-01-22 12:45发布

问题:

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?

回答1:

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.

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 of mine):

PS> 'abc', 'cdefg' | Select-Object -ExpandProperty Length
3
5


回答2:

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 }