Multiple csv files to one csv file - Powershell - powershell

I have been reading through some of the previous posts about the concept, but all the solutions i find very different from eachother.
I have multiple csv's devided over seperate folders (all with kind of the same path but the subfolders are different).
I now need to import these csv's, change the headers and export it into one single csv.
I have been trying with this but im getting a very weird error: Import-Csv : Cannot open file "C:\Windows\system32\Book1.csv"
Although my path is refering to C:\csv ?
$CSVFolder = 'C:\csv\'; #'
$OutputFile = 'C:\export\test3.csv';
$CSV= #();
Get-ChildItem -Path $CSVFolder -Filter *.csv | ForEach-Object {
$CSV += #(Import-Csv -Path $_)
}
$CSV | Export-Csv -Path $OutputFile -NoTypeInformation -Force;
I was thinking of using a datatable for the headers, but its not so clear to me at this point. Also the issue with the error is not clear for me...

As has been noted:
Import-Csv -Path $_ should be Import-Csv -Path $_.FullName in your code,
(Strictly speaking, it should be Import-Csv -LiteralPath $_.FullName, because strings passed to the -Path parameter are subject to wildcard resolution).
because the [System.IO.FileInfo] instances representing files returned by Get-ChildItem are converted to strings when they are passed to the -Path (or -LiteralPath) parameter as a command-line argument (as opposed to via the pipeline), in which case the the mere file name rather than the full path is used, if your Get-ChildItem command targets a directory in Windows PowerShell (see background information below).
A mere filename such as Book1.csv is interpreted as relative to the current directory (which happened to be C:\Windows\system32 in your case), so Import-Csv looks for file C:\Windows\system32\Book1.csv rather than the intended C:\csv\Book1.csv.
Note that piping Get-ChildItem output to cmdlets is generally not affected by this, because the .PSPath property (which PowerShell adds behind the scenes) containing the full path (including PS provider prefix) binds to the -LiteralPath parameter.
Note that as of PSv5.1.14393.693, however, this mechanism is broken for Import-Csv, due to a bug.
This is a common pitfall that occurs whenever [System.IO.FileInfo] instances are passed to cmdlets that accept file paths via [string](-array)-typed parameters as arguments.
To be safe: Always use .FullName when you pass objects received from Get-ChildItem to another command as a parameter value (as opposed to via the pipeline) to ensure that the full path is passed.
Optional background information:
This behavior is a pitfall, because it is perfectly reasonable to assume that passing a [System.IO.FileInfo] instance as-is to a command that accepts file paths works, given the object-oriented nature of PowerShell - especially, since it does work reliably when using the pipeline rather than a parameter value.
Unfortunately, the built-in cmdlets that accept file paths (-Path, -LiteralPath parameters) do so as [string]s only (there is no parameter set that accepts [System.IO.FileInfo] instances directly), and it is in the course of [System.IO.FileInfo]-to-string conversion that the problem arises.
There also wouldn't be a problem if the [System.IO.FileInfo] instances consistently evaluated to the files' full paths, which is unfortunately not the case in Windows PowerShell (this has since been fixed in PowerShell Core):
Get-ChildItem <directory> outputs [System.IO.FileInfo] instances that evaluate to file names only in a string context.
Get-ChildItem <literalFilePathOrWildCardExpr> outputs [System.IO.FileInfo] instances that evaluate to full paths.
In other words: It is only if Get-ChildItem targets a directory (folder) that the objects it returns evaluate to their file names only in a string context.
Targeting a specific file or using a wildcard expression results in full paths, by contrast; with Get-Item, that's always the case.

You simply need to 'fullname' property, instead of 'name'.
Ex:
PS /Users/adil/Downloads> gi *csv |select name
Name
----
tradesdownload.csv
PS /Users/adil/Downloads> gi *csv |select name, fullname
Name FullName
---- --------
tradesdownload.csv /Users/adil/Downloads/tradesdownload.csv

try this code. This code take all csv file, import them and take only column 1, 2, 3 and change column name to header1, header2, header3, then export all into new csv file
Get-ChildItem "C:\temp2" -Filter "*.csv" |
%{Import-Csv $_.FullName -Header header1, header3,header4} |
Export-Csv "c:\temp\result.csv" -NoTypeInformation
#a short version (for no purist)
gci "C:\temp2" -Filter "*.csv" | %{ipcsv $_.FullName -Header header1, header3,header4} | epcsv "c:\temp\result.csv" -NoType

Related

Foreach/copy-item based on name contains

I'm trying to create a list of file name criteria (MS Hotfixes) then find each file name containing that criteria in a directory and copy it to another directory. I think I'm close here but missing something simple.
Here is my current attempt:
#Create a list of the current Hotfixes.
Get-HotFix | Select-Object HotFixID | Out-File "C:\Scripts\CurrentHotfixList.txt"
#
#Read the list into an Array (dropping the first 3 lines).
$HotfixList = Get-Content "C:\Scripts\CurrentHotfixList.txt" | Select-Object -Skip 3
#
#Use the Hotfix names and copy the individual hotfixes to a folder
ForEach ($Hotfix in $HotfixList) {
Copy-Item -Path "C:\KBtest\*" -Include *$hotfix* -Destination "C:\KBtarget"
}
If I do a Write-Host $Hotfix and comment out my Copy-Item line I get the list of hotfixes as expected.
If I run just the copy command and input the file name I am looking for - it works.
Copy-Item -Path "C:\KBtest\*" -Include *kb5016693* -Destination "C:\KBtarget"
But when I run my script it copies all the files in the folder and not just the one file I am looking for. I have several hotfixes in that KBtest folder but only one that is correct for testing.
What am I messing up here?
The simplest solution to your problem, taking advantage of the fact that -Include can accept an array of patterns:
# Construct an array of include patterns by enclosing each hotfix ID
# in *...*
$includePatterns = (Get-HotFix).HotfixID.ForEach({ "*$_*" })
# Pass all patterns to a single Copy-Item call.
Copy-Item -Path C:\KBtest\* -Include $includePatterns -Destination C:\KBtarget
As for what you tried:
To save just the hotfix IDs to a plain-text file, each on its own line, use the following, don't use Select-Object -Property HotfixId (-Property is implied if you omit it), use Select-Object -ExpandProperty HotfixId:
Get-HotFix | Select-Object -ExpandProperty HotFixID | Out-File "C:\Scripts\CurrentHotfixList.txt"
Or, more simply, using member-access enumeration:
(Get-HotFix).HotFixID > C:\Scripts\CurrentHotfixList.txt
Using Select-Object -ExpandProperty HotfixID or (...).HotfixID returns only the values of the .HotfixID properties, whereas Select-Object -Property HotfixId - despite only asking for one property - returns an object that has a .HotfixID property - this is a common pitfall; see this answer for more information.
Then you can use a Get-Content call alone to read the IDs (as strings) back into an array (no need for Select-Object -Skip 3):
$HotfixList = Get-Content "C:\Scripts\CurrentHotfixList.txt"
(Note that, as the solution at the top demonstrates, for use in the same script you don't need to save the IDs to a file in order to capture them.)
This will likely fix your primary problem, which stems from how Out-File creates for-display string representations of the objects ([pscustomobject] instances) that Select-Object -Property HotfixID created:
Not only is there an empty line followed by a table header at the start of the output (which your Select-Object -Skip 3 call skips), there are also two empty lines at the end.
When these empty lines were read into $hotfix in your foreach loop, -Include *$hotfix* effectively became -Include **, which therefore copied all files.
first, you do not need to create and import those textfiles:
get-hotfix | ?{$_.hotfixid -notmatch 'KB5016594|KB5004567|KB5012170'} | %{
copy-item -path "C:\kbtest\$($_.HotfixId).exe" -Destination "C:\kbTarget"
}
This filters for the hotfixes you do not want, if you do not need it remove:
?{$_.hotfixid -notmatch 'KB5016594|KB5004567|KB5012170'}
I assume that those files are exe files in my example.

PowerShell script that accepts pipeline input?

Need a little help to tweak this script that copies the latest file to another folder:
$FilePath = "C:\Downloads\Sales 202112*.xlsx"
$DestinationPath = "C:\myFiles\"
gci -Path $FilePath -File |
Sort-Object -Property LastWriteTime -Descending |
Select FullName -First 1 |
Copy-Item $_ -Destination $DestinationPath
Not sure how to reference pipeline input for the Copy-Item command.
Thanks.
tl;dr
Get-ChildItem -Path $FilePath -File |
Sort-Object -Property LastWriteTime -Descending |
Select-Object -First 1 | # Note: No 'FullName'
Copy-Item -Destination $DestinationPath # Note: No '$_'
The simplest and most robust approach is to pipe Get-ChildItem / Get-Item output as-is to other file-processing cmdlets, which binds to the latter's -LiteralPath parameter, i.e the input file path.
As for what you tried:
The automatic $_ variable, which explicitly refers to the pipeline input object at hand, is only needed (and supported) inside script blocks ({ ... }) passed to cmdlets.
With suitable pipeline input, PowerShell implicitly binds it to a pipeline-binding parameter of the target cmdlet, which in case of the Copy-Item call above is -LiteralPath. In other words: specifying a value for the target parameter as an argument isn't necessary.
This answer explains the mechanism that binds the System.IO.FileInfo and System.IO.DirectoryInfo instances that Get-ChildItem outputs to the -LiteralPath parameter of file-processing cmdlets such as Copy-Item.
Note that, with FullName out of the picture (see below), it is indeed a System.IO.FileInfo instance that Copy-Item receives as pipeline input, because the Sort-Object ... and Select-Object -First 1 calls above pass them through without changing their type.
Selecting the FullName property in an attempt to pass only the file's full path (as a string) via the pipeline is unnecessary, and, more importantly:
It fails, unless you use -ExpandProperty FullName to ensure that only the property value is output; without it, you get an object that has a .FullName property - see this answer for more information.
Even if you fix that, the solution is - at least hypothetically - less robust than passing the System.IO.FileInfo instances as a whole: mere string input binds to the -Path rather than the -LiteralPath parameter, which means that what is by definition a literal path is interpreted as a wildcard expression, which notably causes mishandling of literal paths that contain [ characters (e.g. c:\foo\file[1].txt).
See this answer for how to inspect the specifics of the pipeline-binding parameters of any given cmdlet.

unable to read contents of file

I'm attempting to read the contents of a file:
$releaseNotesPath = "$(System.DefaultWorkingDirectory)\_ccp-develop\ccp\ccp\ReleaseNotes\ReleaseNotes\"
$latestReleaseNotesFile = Get-ChildItem -Path $releaseNotesPath -Filter *.txt | Select-Object FullName,Name | Sort-Object -Property Name | Select-Object -First 1
The issue occurs here:
$releaseNote = Get-Content $latestReleaseNotesFile
2021-11-14T14:29:07.0729088Z ##[error]Cannot find drive. A drive with the name '#{FullName=D' does not exist.
2021-11-14T14:29:07.1945879Z ##[error]PowerShell exited with code '1'.
What am I doing wrong?
You need to provide the file path (FullName):
$releaseNote = Get-Content $latestReleaseNotesFile.FullName
Shayki Abramczyk already answered how, I'll chime in with the why part.
So, let's see what goes on, step by step
# Assign a value to variable, simple enough
$latestReleaseNotesFile =
# Get a list of all
Get-ChildItem -Path $releaseNotesPath -Filter *.txt |
# Interested only on file full name and shortname. Here's the catch
Select-Object FullName,Name |
# Sort the results by name
Sort-Object -Property Name |
# Return the first object of collection.
Select-Object -First 1
Note that in the catch part, you are implicitly creating a new, custom Powershell object that contains two members: a fully qualified file name and short name. When you pass the custom object later to Get-Content, it doesn't know how to process the custom object. So, thus the error. Shayki's answer works, as it explicitly tells to use the FullName member that contains, well file's full name.
There's good information in the existing answers; let me summarize and complement them:
A simplified and robust reformulation of your command:
$latestReleaseNotesFile =
Get-ChildItem -LiteralPath $releaseNotesPath -Filter *.txt |
Select-Object -First 1
$releaseNote = $latestReleaseNotesFile | Get-Content
Get-ChildItem -LiteralPath parameter ensures that its argument is treated literally (verbatim), as opposed to as a wildcard expression, which is what -Path expects.
Get-ChildItem's output is already sorted by name (while this fact isn't officially documented, it is behavior that users have come to rely on, and it won't change).
By not using Select-Object FullName, Name to transform the System.IO.FileInfo instances output by Get-ChildItem to create [pscustomobject] instances with only the specified properties, the resulting object can as a whole be piped to Get-Content, where it is implicitly bound by its .PSPath property value to -LiteralPath (whose alias is -PSPath), which contains the full path (with a PowerShell provider prefix).
See this answer for details on how this pipeline-based binding works.
As for what you tried:
Get-Content $latestReleaseNotesFile
This positionally binds the value of variable $latestReleaseNotesFile to the Get-Content's -Path parameter.
Since -Path is [string[]]-typed (i.e., it accepts one or more strings; use Get-Help Get-Content to see that), $latestReleaseNotesFile's value is stringified via its .ToString() method, if necessary.
Select-Object FullName, Name
This creates [pscustomobject] instances with with .FullName and .Name properties, whose values are taken from the System.IO.FileInfo instances output by Get-ChildItem.
Stringifying a [pscustomobject] instance yields an informal, hashtable-like representation suitable only for the human observer; e.g.:
# -> '#{FullName=/path/to/foo; Name=foo})'
"$([pscustomobject] #{ FullName = '/path/to/foo'; Name = 'foo' }))"
Note: I'm using an expandable string ("...") to stringify, because calling .ToString() directly unexpectedly yields the empty string, due to a longstanding bug described in GitHub issue #6163.
Unsurprisingly, passing a string with content #{FullName=/path/to/foo; Name=foo}) is not a valid file-system path, and resulted in the error you saw.
Passing the .FullName property value instead, as shown in Shayki's answer, solves that problem:
For full robustness, it is preferable to use -LiteralPath instead of the (positionally implied) -Path
Specifically, paths that contain verbatim [ or ] will otherwise be misinterpreted as a wildcard expression.
Get-Content -LiteralPath $latestReleaseNotesFile.FullName
As shown at the top, sticking with System.IO.FileInfo instances and providing them via the pipeline implicitly binds robustly to -LiteralPath:
# Assumes that $latestReleaseNotesFile is of type [System.IO.FileInfo]
# This is the equivalent of:
# Get-Content -LiteralPath $latestReleaseNotesFile.PSPath
$latestReleaseNotesFile | Get-Content
Pitfall: One would therefore expect that passing the same type of object as an argument results in the same binding, but that is not true:
# !! NOT the same as:
# $latestReleaseNotesFile | Get-Content
# !! Instead, it is the same as:
# Get-Content -Path $latestReleaseNotesFile.ToString()
Get-Content $latestReleaseNotesFile
That is, the argument is not bound by its .PSPath property value to -LiteralPath; instead, the stringified value is bound to -Path.
In PowerShell (Core) 7+, this is typically not a problem, because System.IO.FileInfo (and System.IO.DirectoryInfo) instances consistently stringify to their full path (.FullName property value) - however, it still malfunctions for literal paths containing [ or ].
In Windows PowerShell, such instances situationally stringify to the file name (.Name) only, making malfunctioning and subtle bugs likely - see this answer.
This problematic asymmetry is discussed in GitHub issue #6057.
The following is a summary of the above with concrete guidance:
Robustly passing file-system paths to file-processing cmdlets:
Note: The following applies not just to Get-Content, but to all file-processing standard cmdlets - with the unfortunate exception of Import-Csv in Windows PowerShell, due to a bug.
as an argument:
Use -LiteralPath explicitly, because using -Path (which is also implied if neither parameter is named) interprets its argument as a wildcard expression, which notably causes literal file paths containing [ or ] to be misinterpreted.
# $pathString is assumed to be a string ([string])
# OK: -LiteralPath ensures interpretation as a literal path.
Get-Content -LiteralPath $pathString
# Same as:
# Get-Content -Path $pathString
# !! Path is treated as a *wildcard expression*.
# !! This will often not matter, but breaks with paths with [ or ]
Get-Content $pathString
Additionally, in Windows PowerShell, when passing a System.IO.FileInfo or System.IO.DirectoryInfo instance, explicitly use the .FullName (file-system-native path) or .PSPath property (includes a PowerShell provider prefix; path may be based on a PowerShell-specific drive) to ensure that its full path is used; this is no longer required in PowerShell (Core) 7+, where such instances consistently stringify to their .FullName property - see this answer.
# $fileSysInfo is assumed to be of type
# [System.IO.FileInfo] or [System.IO.DirectoryInfo].
# Required for robustness in *Windows PowerShell*, works in both editions.
Get-Content -LiteralPath $fileSysInfo.FullName
# Sufficient in *PowerShell (Core) 7+*:
Get-Content -LiteralPath $fileSysInfo
via the pipeline:
System.IO.FileInfo and System.IO.DirectoryInfo instances, such as emitted by Get-ChildItem and Get-Item, can be passed as a whole, and robustly bind to -LiteralPath via their .PSPath property values - in both PowerShell editions, so you can safely use this approach in cross-edition scripts.
# Same as:
# Get-Content -LiteralPath $fileSysInfo.PSPath
$fileSysInfo | Get-Content
This mechanism - explained in more detail in this answer - relies on a property name matching a parameter name, including the parameter's alias names. Therefore, input objects of any type that have either a .LiteralPath, a .PSPath, or, in PowerShell (Core) 7+ only, a .LP property (all alias names of the -LiteralPath parameter) are bound by that property's value.[1]
# Same as:
# Get-Content -LiteralPath C:\Windows\win.ini
[pscustomobject] #{ LiteralPath = 'C:\Windows\win.ini' } | Get-Content
By contrast, any object with a .Path property binds to the wildcard-supporting -Path parameter by that property's value.
# Same as:
# Get-Content -Path C:\Windows\win.ini
# !! Path is treated as a *wildcard expression*.
[pscustomobject] #{ Path = 'C:\Windows\win.ini' } | Get-ChildItem
Direct string input and the stringified representations of any other objects also bind to -Path.
# Same as:
# Get-Content -Path C:\Windows\win.ini
# !! Path is treated as a *wildcard expression*.
'C:\Windows\win.ini' | Get-Content
Pitfall: Therefore, feeding the lines of a text file via Get-Content to Get-ChildItem, for instance, can also malfunction with paths containing [ or ]. A simple workaround is to pass them as an argument to -LiteralPath:
Get-ChildItem -LiteralPath (Get-Content -LiteralPath Paths.txt)
[1] That this logic is only applied to pipeline input, and not also to input to the same parameter by argument is an unfortunate asymmetry discussed in GitHub issue #6057.

Exclude extension from an output file in PS?

My goal is to display items in the directory C:\test in a log file called log.txt without displaying the file-extensions of the files found, e.g. .zip, .pdf, etc.
My script so far:
Get-ChildItem -Path C:\Test\ -name |Out-File C:\test2\log.txt
How do I get the .log file to NOT display the extensions of the files found in the C:\test folder?
Use BaseName property instead of Name:
Get-ChildItem -Path C:\Test\ | Select-Object -ExpandProperty BaseName | Out-File C:\test2\log.txt
As there's no built-in -BaseName property for Get-ChildItem cmdlet, you need to get that property using Select-Object. Expanding the property allows you to get only the value of chosen property, without the header.
Another way to get BaseName value would be to use .BaseName like this:
(Get-ChildItem -Path C:\Test\).BaseName | Out-File C:\test2\log.txt
That form is shorter, but personally I prefer the first one due to readability and no need to remember about surrounding braces ().
Best practice
If you want to inspect what are the possible properties (and their values) of the object you have, you can also use Select-Object for that:
# Warning: HUGE OUTPUT POSSIBLE
Get-ChildItem -Path C:\test\| Select-Object *
# It's usually good to take only one object from the array
$obj = (Get-ChildItem -Path C:\test\)[0]
$obj | Select-Object *

Why different ways of Get-ChildItem filtering give same objects that are actually different?

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?
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.
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