MicrosoftDocs / PowerShell-Docs

The official PowerShell documentation sources
https://learn.microsoft.com/powershell
Creative Commons Attribution 4.0 International
1.93k stars 1.55k forks source link

Get-Item / Remove-Item (and probably more) - providing a `string` vs a `IO.FileSystemInfo` object - behavior is different and undocumented? #11109

Closed chadbaldwin closed 2 months ago

chadbaldwin commented 2 months ago

Prerequisites

Links

Summary

I don't personally feel this is a bug and more so feels like an undocumented behavior. Basically, depending on whether you are passing a string vs a IO.FileSystemInfo object via pipeline to certain cmdlets (e.g. Get-Item, Remove-Item, etc), their behavior is different and unexpected.

If -Path is the default pipeline parameter, then the behavior of passing an item via pipeline vs passing it directly to that parameter should result in the same behavior regardless of the items type, but in this case it does not.

The reason this came up for me is because I had written a script to essentially do this:

foobar.exe | Get-Item | Remove-Item

The script was not doing anything because the file paths returned by foobar.exe contained glob characters. (e.g. [foobar].xyz)

So I fixed the script to run as:

foobar.exe | % { Get-Item -LiteralPath $_ } | Remove-Item

And then I started wondering why I had to make that fix for Get-Item but not Remove-Item...then I realized one was being passed a list of strings and the other a list of IO.FileSystemInfo objects. This led me to check the documentation to see if this was mentioned in there.

Details

Code example:

$filename = '[foobar].xyz'
$null = New-Item -ItemType File -Name $filename -Force
$fileobj = Get-Item -LiteralPath $filename

# Expected: This will find nothing, because the string has glob characters, so `-Path` will treat it as such
Get-Item -Path $filename

# Expected: This will find the file because the string is interpreted literally
Get-Item -LiteralPath $filename

# Expected: This will find nothing, because the default pipeline parameter is `-Path`, which again, will treat it as a glob
$filename | Get-Item

# Expected: This will find nothing. `-Path` parameter wants [string], so `.ToString()` is called on `$fileobj` resulting in a file path which appears as a glob
Get-Item -Path $fileobj

# Expected: This will find the file. `-LiteralPath` parameter wants [string], so `.ToString()` is called on `$fileobj`, the path returned is interpreted literally and the file is found
Get-Item -LiteralPath $fileobj

<# Unexpected: This will find the file.
               The reason this is unexpected is because the default pipeline parameter for `Get-Item` is `-Path`, which as displayed above, should not find the file.
               So, I assume there must be special handling of `IO.FileSystemInfo` objects when supplied via pipeline.
#>
$fileobj | Get-Item

Suggested Fix

There should be some sort of note indicating that for these cmdlets, if a IO.FileSystemInfo object is passed in via the pipeline, then it is handled directly, rather than via the -Path parameter; Whereas if a string is passed in via the pipeline, is is interpreted the same as -Path.

JamesDBartlett3 commented 2 months ago

I agree, it's really not obvious at all from the documentation that many PowerShell cmdlets will fail to properly parse file paths containing special characters, unless the developer explicitly adds the -LiteralPath parameter. In fact, I recently submitted a PR to fix an issue in Microsoft/Analysis-Services where a function was invoking Set-Content with an implicit -Path parameter rather than an explicit -LiteralPath, and failing on file paths containing square brackets as a result. I think this default behavior, and all of its potential downsides, should be plainly and precisely documented in the PowerShell docs.

sdwheeler commented 2 months ago

It is already documented by the fact that -Path can accept pipeline input ByPropertyName or ByValue, and -LiteralPath can accept pipeline input ByPropertyName only. Pipeline Parameter Binding resolves which parameter that the input is bound to. When you pipe a string, it can only be bound to -Path. When you pipe a FileInfo object, it binds to -LiteralPath ByPropertyName.

The point is that this isn't undocumented handling of FileInfo objects, it is standard parameter binding.

You can see this using Trace-Command. This example shows binding of the string input to -Path.

PS> Trace-Command -Name ParameterBinding -Expression {$filename | Get-Item } -PSHost
DEBUG: 2024-05-15 08:58:01.9189 ParameterBinding Information: 0 : BIND NAMED cmd line args [Get-Item]
DEBUG: 2024-05-15 08:58:01.9191 ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Get-Item]
DEBUG: 2024-05-15 08:58:01.9193 ParameterBinding Information: 0 : BIND cmd line args to DYNAMIC parameters.
DEBUG: 2024-05-15 08:58:01.9194 ParameterBinding Information: 0 :     DYNAMIC parameter object: [Microsoft.PowerShell.Commands.FileSystemProviderGetItemDynamicParameters]
DEBUG: 2024-05-15 08:58:01.9196 ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Get-Item]
DEBUG: 2024-05-15 08:58:01.9197 ParameterBinding Information: 0 : CALLING BeginProcessing
DEBUG: 2024-05-15 08:58:01.9199 ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Get-Item]
DEBUG: 2024-05-15 08:58:01.9201 ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.String]
DEBUG: 2024-05-15 08:58:01.9202 ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: 2024-05-15 08:58:01.9203 ParameterBinding Information: 0 :     Parameter [Path] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: 2024-05-15 08:58:01.9205 ParameterBinding Information: 0 :     BIND arg [[foobar].xyz] to parameter [Path]
DEBUG: 2024-05-15 08:58:01.9206 ParameterBinding Information: 0 :         Binding collection parameter Path: argument type [String], parameter type [System.String[]], collection type Array, element type [System.String], no coerceElementType
DEBUG: 2024-05-15 08:58:01.9207 ParameterBinding Information: 0 :         Creating array with element type [System.String] and 1 elements
DEBUG: 2024-05-15 08:58:01.9208 ParameterBinding Information: 0 :         Argument type String is not IList, treating this as scalar
DEBUG: 2024-05-15 08:58:01.9210 ParameterBinding Information: 0 :         Adding scalar element of type String to array position 0
DEBUG: 2024-05-15 08:58:01.9212 ParameterBinding Information: 0 :         BIND arg [System.String[]] to param [Path] SUCCESSFUL
DEBUG: 2024-05-15 08:58:01.9213 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: 2024-05-15 08:58:01.9215 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: 2024-05-15 08:58:01.9216 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName WITH COERCION
DEBUG: 2024-05-15 08:58:01.9217 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName WITH COERCION
DEBUG: 2024-05-15 08:58:01.9219 ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Get-Item]
DEBUG: 2024-05-15 08:58:01.9220 ParameterBinding Information: 0 : CALLING ProcessRecord
DEBUG: 2024-05-15 08:58:01.9228 ParameterBinding Information: 0 : CALLING EndProcessing

This example shows binding by property name. In this case, PSPath is an alias for LiteralPath, so the PSPath property is binding to the -LiteralPath parameter.

PS> Trace-Command -Name ParameterBinding -Expression {$fileobj | Get-Item } -PSHost
DEBUG: 2024-05-15 08:59:18.2210 ParameterBinding Information: 0 : BIND NAMED cmd line args [Get-Item]
DEBUG: 2024-05-15 08:59:18.2212 ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Get-Item]
DEBUG: 2024-05-15 08:59:18.2214 ParameterBinding Information: 0 : BIND cmd line args to DYNAMIC parameters.
DEBUG: 2024-05-15 08:59:18.2216 ParameterBinding Information: 0 :     DYNAMIC parameter object: [Microsoft.PowerShell.Commands.FileSystemProviderGetItemDynamicParameters]
DEBUG: 2024-05-15 08:59:18.2217 ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Get-Item]
DEBUG: 2024-05-15 08:59:18.2219 ParameterBinding Information: 0 : CALLING BeginProcessing
DEBUG: 2024-05-15 08:59:18.2221 ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Get-Item]
DEBUG: 2024-05-15 08:59:18.2222 ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.IO.FileInfo]
DEBUG: 2024-05-15 08:59:18.2223 ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: 2024-05-15 08:59:18.2225 ParameterBinding Information: 0 :     Parameter [Path] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: 2024-05-15 08:59:18.2227 ParameterBinding Information: 0 :     BIND arg [D:\temp\test\[foobar].xyz] to parameter [Path]
DEBUG: 2024-05-15 08:59:18.2229 ParameterBinding Information: 0 :         Binding collection parameter Path: argument type [FileInfo], parameter type [System.String[]], collection type Array, element type [System.String], no coerceElementType
DEBUG: 2024-05-15 08:59:18.2230 ParameterBinding Information: 0 :         Creating array with element type [System.String] and 1 elements
DEBUG: 2024-05-15 08:59:18.2231 ParameterBinding Information: 0 :         Argument type FileInfo is not IList, treating this as scalar
DEBUG: 2024-05-15 08:59:18.2233 ParameterBinding Information: 0 :         BIND arg [D:\temp\test\[foobar].xyz] to param [Path] SKIPPED
DEBUG: 2024-05-15 08:59:18.2234 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: 2024-05-15 08:59:18.2236 ParameterBinding Information: 0 :     Parameter [Path] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: 2024-05-15 08:59:18.2237 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: 2024-05-15 08:59:18.2238 ParameterBinding Information: 0 :     Parameter [LiteralPath] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: 2024-05-15 08:59:18.2240 ParameterBinding Information: 0 :     BIND arg [Microsoft.PowerShell.Core\FileSystem::D:\temp\test\[foobar].xyz] to parameter [LiteralPath]
DEBUG: 2024-05-15 08:59:18.2241 ParameterBinding Information: 0 :         Binding collection parameter LiteralPath: argument type [String], parameter type [System.String[]], collection type Array, element type [System.String], no coerceElementType
DEBUG: 2024-05-15 08:59:18.2243 ParameterBinding Information: 0 :         Creating array with element type [System.String] and 1 elements
DEBUG: 2024-05-15 08:59:18.2244 ParameterBinding Information: 0 :         Argument type String is not IList, treating this as scalar
DEBUG: 2024-05-15 08:59:18.2245 ParameterBinding Information: 0 :         Adding scalar element of type String to array position 0
DEBUG: 2024-05-15 08:59:18.2246 ParameterBinding Information: 0 :         BIND arg [System.String[]] to param [LiteralPath] SUCCESSFUL
DEBUG: 2024-05-15 08:59:18.2248 ParameterBinding Information: 0 :     Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName WITH COERCION
DEBUG: 2024-05-15 08:59:18.2250 ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Get-Item]
DEBUG: 2024-05-15 08:59:18.2251 ParameterBinding Information: 0 : CALLING ProcessRecord
DEBUG: 2024-05-15 08:59:18.2254 ParameterBinding Information: 0 : CALLING EndProcessing

    Directory: D:\temp\test

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           5/15/2024  8:47 AM              0 [foobar].xyz
sdwheeler commented 2 months ago

You could also do it this way:

Get-Item (foobar.exe) | Remove-Item

You can also use a delay-bind script block:

foobar.exe | Remove-Item -LiteralPath { $_ }