Home > Software engineering >  PowerShell - Best practice working with files
PowerShell - Best practice working with files

Time:11-11

For reasons I'm starting to regret, I got into the habit of just working with [System.IO.FileInfo] and now I'm wondering if there is a best practice around avoiding that and just using full names to the file(s) or if there is a different workaround for my current conundrum.

I need to make a lot of my smaller scripts work with powershell.exe vs. pwsh.exe because they're going to be used by folks and computers that don't have PowerShell (Core) installed - but every once in a while there arises an issue. This time it is the handling of whatever is returned from Get-ChildItem and the fact that Windows PowerShell doesn't give you the full path like PowerShell (Core) does. One workaround I have would be to force the full name ($file.FullName), but that in turn breaks the fact that I'm accustomed to working with System.IO.FileInfo variables.

So first question without examples: What is the best practice? Should I have been using System.IO.FileInfo in the first place?

Second question, with examples: Is there a better way to handle this so that Windows PowerShell and PowerShell (Core) act consistently?

Consider the following - at this point I would probably call a function to act on each qualifying input file (using filtering on name or file extension, etc. to get the right set).

PS C:\tmp> (Get-ChildItem -LiteralPath $PWD -File).ForEach({Write-Host "The input file will be: $($_)"})

In PowerShell (Core) - Write-Host doesn't exactly show you the FileInfo Object, but it shows the difference between the two.

The input file will be: C:\tmp\Another File.txt
The input file will be: C:\tmp\CSV File.csv
The input file will be: C:\tmp\First File.txt
The input file will be: C:\tmp\New Rich Text Document.rtf

Windows PowerShell however resolves to the file name only (which in turn actually breaks the function if the file isn't in the working directory, or worse causes updates to a file that weren't intended if the working directory happens to have a file with the same name as I'm expecting to work on).

The input file will be: Another File.txt
The input file will be: CSV File.csv
The input file will be: First File.txt
The input file will be: New Rich Text Document.rtf

So - what's my best course of action? I like the FileInfo objects themselves because they have some handy properties like BaseName, Directory, DirectoryName, and Extension. There are probably also a number of useful methods available that I might be using in my functions.

If I just essentially pass $_.FullName to the function, then within the function it is a string and I'll need to use Split-Path among other things to get similar properties that I'm working with

CodePudding user response:

Yes, the inconsistent stringification of [System.IO.FileInfo] instances in Windows PowerShell is unfortunate - sometimes by file name only, other times by full path (everything in this answer applies analogously to System.IO.DirectoryInfo instances).

  • As an aside: The problem generally wouldn't exist if passing a pipeline-binding parameter's value as an argument exhibited the same behavior as via the pipeline - see GitHub issue #6057

You can stick with [System.IO.FileInfo] instances and use the following idiom when you need to pass them to cmdlets that have pipeline-binding -LiteralPath parameters, such as Get-Item and Get-ChildItem, which works robustly in both PowerShell editions; e.g.:

$someFileInfoInstance | GetChild-Item

A System.IO.FileInfo bound this way - assuming that it wasn't constructed directly - binds via its PowerShell provider-supplied .PSPath property value, which is always its full path. The reason that it is the .PSPath property value that is bound is that -PSPath is defined as an alias of the -LiteralPath parameter, combined with declaring the latter as ValueFromPipelineByPropertyName

CodePudding user response:

There was a lengthy discussion about this when the change was made to default ToString to result in a full path instead of a relative path.

TL;DR

Steve Lee stated the best practice is to explicitly declare whether you want the full path or the relative path by using $_.Name or $_.Fullname.

Bad Practice ❌

$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $fileInfo"

Best Practice ✅

$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $($fileInfo.FullName)"

Details

Get-ChildItem returns a System.IO.FileInfo object in both Windows PowerShell and PowerShell Core. The problem you're encountering is with the implementation of the ToString method of the System.IO.FileInfo object.

You type and run this:

$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $fileInfo"

...which gets translated into this:

$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $(fileInfo.ToString())"

..which gets translated to this on Windows PowerShell:

$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $(fileInfo.Name)"

...and this on PowerShell Core:

$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $(fileInfo.FullName)"

The reason they moved from using .Name to .FullName for the default implementation of ToString appears to be something related to security because relative paths could be tinkered with.

The reason it's a best practice to explicitly convert objects into strings instead of relying on the object to figure out how to convert itself into a string is because of exactly this scenario, the implementation could change and leave you up a creek.

  • Related