1

For several unrelated reasons I'm playing with several ways to deal with filtering the pipeline output when usign the Get-ChildItem command.

I've created a short code to exemplify what I mean. When I use different ways to get the same item, the item gets "stringified" differently depending on the way it was found, even when it's the same item every time.

Lets say we have this folder C:\Folder1\Folder1-a with File1.7z and File2.txt inside. There are more File1-X folders inside Folder1, each with a .7z and a .txt file inside, and their names can have some special characters like square brackets. This is no relevant for this question, but it's the reason of why I would prefer to use some specific way of filtering over another (explained in the comments of the attached code).

Here's the code that exemplifies my point:

#Initialize 7zip
if (-not (test-path "$env:ProgramFiles\7-Zip\7z.exe")) {throw "$env:ProgramFiles\7-Zip\7z.exe needed"}
set-alias 7zip "$env:ProgramFiles\7-Zip\7z.exe"

#this is a placeholder, $TXTFile would be the result of an iteration over a previous Item array
$TXTFile = Get-Item -Path "C:\Folder1\Folder1-a\File2.txt"

#Now there comes 3 different ways to select the 7z file in the same directory as File2.txt

# I use this to modify the path name so square brackets are properly escaped
$directory1 =$TXTFile.DirectoryName -replace "\[","`````[" -replace "\]","`````]"
[array]$7ZFile1 = Get-ChildItem -File -Path "$directory1\*.7z"

# This option uses LiteralPath so no modification is needed.
# More convenient since it supports any kind of special character in the name.
$directory2=$TXTFile.DirectoryName
[array]$7ZFile2= Get-ChildItem -File -LiteralPath $directory2 -Filter *.7z

# This option uses LiteralPath so no modification is needed.
# More convenient since it supports any kind of special character in the name.
$directory3=$TXTFile.DirectoryName
[array]$7ZFile3 = Get-ChildItem -File -LiteralPath $directory3 | Where-Object {$_.Extension -eq ".7z"}

#Lets see each item. They all seem equal
$7ZFile1
$7ZFile2
$7ZFile3
Write-Host "`n"

#Lets see how they have he same FullName
Write-Host $7ZFile1.FullName
Write-Host $7ZFile2.FullName
Write-Host $7ZFile3.FullName
Write-Host "`n"

#Lets compare them using -eq. Damn, they are not equal
if($7ZFile1 -eq $7ZFile2){"7ZFile1=7ZFile2"}Else{"7ZFile1!=7ZFile2"}
if($7ZFile2 -eq $7ZFile3){"7ZFile2=7ZFile3"}Else{"7ZFile2!=7ZFile3"}
if($7ZFile3 -eq $7ZFile1){"7ZFile3=7ZFile1"}Else{"7ZFile3!=7ZFile1"}
Write-Host "`n"

#This is relevant if we "stringify" each object. First one returns FullName, the two others return Name
Write-Host $7ZFile1
Write-Host $7ZFile2
Write-Host $7ZFile3
Write-Host "`n"

#Example of this being relevant. Inside File1.7z is a txt file. If you use 7zip por example like this:
7zip t $7ZFile1 *.txt -scrc     #Success
7zip t $7ZFile2 *.txt -scrc     #Fail, can't find 7ZFile2
7zip t $7ZFile3 *.txt -scrc     #Fail, can't find 7ZFile3

I'm using $7ZFile.FullName to consistently always get the string that I want, however I would like to know why does this happen? why is there a difference in the first place?

js2010
  • 23,033
  • 6
  • 64
  • 66
rovda
  • 221
  • 2
  • 10
  • Why does _what_ happen? We can't see your screen – Mathias R. Jessen Oct 12 '19 at 14:28
  • You're right, I added some clarification, however I had already written how to set up everything and attached a commented script that explains the whole thing in detail. You don't need to see my screen you only need to execute the same script and see your own screen. In any case, the thing is that different ways of filtering the Get-ChildItem pipeline resulting in the exact same item, for some reason behave differently. They look the same, they point to the same file, but when "stringified" they eturn different things. – rovda Oct 12 '19 at 14:31
  • Not directly related to the question, but you may want to use `where.exe` to check for 7z like: `if (!(where.exe 7z 2>$null)) {<# 7-zip does not exist #>;exit}` – AP. Oct 13 '19 at 03:23
  • What would the advantage of using that would be @AP.? – rovda Oct 16 '19 at 23:54
  • 1
    This allows you to verify that `7z` is somewhere in path and executable, but is not restricted to the path you hardcoded. (supports: custom installation, non-standard windows drives, etc) – AP. Oct 17 '19 at 00:08

2 Answers2

1

There are two unrelated problems here:

  • In Windows PowerShell - but fortunately no longer in PowerShell Core - the System.IO.DirectoryInfo and System.IO.FileInfo instances that Get-ChildItem outputs situationally stringify differently - mere filename vs. full path - depending on the specifics of the Get-ChildItem call.

    • See this answer for details.
    • The underlying cause is an inconsistency in the .NET types involved, which has been corrected in .NET Core (on which PowerShell Core is built), as explained in this comment on GitHub.
    • To be safe, always use the .FullName property when passing instances as arguments to other commands or when the intent is to stringify by full name.

    • A simple demonstration of the problem:

# Full-name stringification, due to targeting a *file* (pattern).
PS> (Get-ChildItem $PSHOME\powershell.exe).ToString()
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe

# Name-only stringification, due to targeting a *directory* by *literal* name 
# (even though a filter to get a file is applied) and 
# not also using -Include / -Exclude
PS> (Get-ChildItem $PSHOME -Filter powershell.exe).ToString()
powershell.exe
  • System.IO.DirectoryInfo and System.IO.FileInfo are reference types, which means that their instances are compared using reference equality: that is, two variables containing instances only compare the same if they point to the very same object in memory.

    • Therefore, $7ZFile1 -eq $7ZFile2 is never $true if the two instances were obtained by different Get-ChildItem calls; the best approach is to compare instances by their .FullName property.

    • See this answer for more information about reference equality vs. value equality.

    • A simple demonstration of the reference-equality behavior:

PS> (Get-ChildItem $PSHOME\powershell.exe) -eq (Get-ChildItem $PSHOME\powershell.exe)
False  # Distinct FileInfo objects, which aren't reference-equal.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you so much! I don't know if I should open a new question or not, but I've googled wide and far, and I found nothing. Is there a way to compare two objects? I mean, I know `Compare-Object`, but it only checks for same "names", so to speak (sory for my noobness in the matter), is there some built-in function that iterates through evey parameter and method and compares them? I tried comparing hashes (`GetHashCode()`)of "identically declared" objects, but they returned different hashes, so I'm running out of ideas. – rovda Oct 16 '19 at 23:51
  • 1
    @rovda: There is no _guaranteed_ method for comparing two objects of any type. For _value types_, the standard `-eq` is enough, but for _reference types_, it isn't. If a given reference type happens to implement `IEquatable`, `-eq` is again sufficient, but by no means all reference types do. If you happen to know of a type's _specific property_ that uniquely identifies it - such as `.FullName` in the case at hand - you can use that. In all other cases you'll have to roll your own comparison logic, such as by recursively comparing the values of all properties, if feasible. – mklement0 Oct 17 '19 at 01:17
  • Thank you, in my case I DO have something that is unique. The script I'm working on goes through my whole library of backed up PS2 games and it performs CRC32 on all of them so I got a quick way of checking file integrity. I then compare that with a manually created database (csv) based on a third party website. I want to sanitize this database si I only look for duplicate hashes (thaty's easy) but in case I find a duplicate I want it to compare each completely, if they're equal, delete one, and if they're not log the error so I can handle it manually. So yeah, I'll have to iterate. – rovda Oct 17 '19 at 07:41
0

It's a common annoyance in PS 5, where the string version of what get-childitem returns doesn't have the full path. It's changed in later versions of PS. Get full path of the files in PowerShell

get-childitem .   | foreach tostring  # not full path
get-childitem .\* | foreach tostring  # full path
js2010
  • 23,033
  • 6
  • 64
  • 66
  • I appreciete the reply but this does not answer my question as to WHY does this happen, but WHAT happens, which I already knew. At least it expands on what version it started happening so thank you anyways. – rovda Oct 12 '19 at 15:19