2
PS C:\Users\robert.lee> new-item C:\temp\junk\delete-me
PS C:\Users\robert.lee> $d=get-childitem C:\temp\junk -file
PS C:\Users\robert.lee> remove-item $d

/* This does NOT delete C:\temp\junk\delete-me, but deletes C:\Users\robert.lee\delete-me. */

PS C:\Users\robert.lee> $d | remove-item

/* This does delete C:\temp\junk\delete-me. */

Why remove-item $d does not know where to find the file (C:\temp\junk) while $d | remove-item does? The object $d has the full path in either case.

PS: remove-item $d does remove file in another location, as tested on PSVersion 7.2.1 on macOS Monterey:

PS /Users/robert.lee/m/Windows> $d=get-childitem /tmp/Junk2 -file  
PS /Users/robert.lee/m/Windows> ls /tmp/Junk2 
delete-me
PS /Users/robert.lee/m/Windows> remove-item $d
PS /Users/robert.lee/m/Windows> ls /tmp/Junk2 
PS /Users/robert.lee/m/Windows> 
puravidaso
  • 1,013
  • 1
  • 5
  • 22
  • 2
    take a look at how the `fileinfo` object converts to a string. it DOES NOT include the full set of info you expect ... but when you pipe to the cmdlet, that cmdlet gets the `fileinfo` object, not a stringified version of the object. – Lee_Dailey May 04 '22 at 02:14

2 Answers2

3

To complement Abraham Zinala's helpful answer with some background information:

From a usability perspective, $d | Remove-Item and Remove-Item $d should work the same; the reasons they don't are (written as of PowerShell Core 7.2.3):

  • Argument-based parameter binding is, unfortunately, less sophisticated than pipeline-based binding:

    • Notably, the ValueFromPipelineByPropertyName property on the -LiteralPath parameter, whose alias is -PSPath, is only honored for input objects provided via the pipeline ($d | Remove-Item) and not also when provided as an argument (Remove-Item $d).

    • With pipeline input, it is the .PSPath property that PowerShell's provider cmdlets decorate their output objects with that robustly binds to -LiteralPath, because the .PSPath property value is a full, provider-qualified path - see this answer for a detailed explanation.

    • This binding asymmetry is the subject of GitHub issue #6057.

  • If you pass a file- or directory-info object as an argument and you do so positionally (i.e. without preceding the argument with the target-parameter name):

    • It is the wildcard-based -Path parameter that is targeted:

      • This is normally not a problem, but does become one if the intended-as-literal path happens to contain [ characters (e.g., c:\foo\file[1].txt), given that [ is a metacharacter in PowerShell's wildcard language.

      • Using a named argument that explicitly targets the -LiteralPath parameter avoids this problem (note that PowerShell (Core) 7+ allows you to shorten to -lp):

        # Note: Still not robust in *Windows PowerShell* - see below.
        Remove-Item -LiteralPath $d
        
    • Both -Path and -LiteralPath are [string[]]-typed, i.e. they accept an array of strings, which in argument-based parameter binding means that any non-string argument is simply stringified (loosely speaking, like calling .ToString() on it)[1]:

      • In Windows PowerShell - as you've experienced - this can lead to subtle but potentially destructive bugs - because System.IO.FileInfo and System.IO.DirectoryInfo instances situationally stringify to only their .Name property, not to their .FullName property - see this answer for details.

      • In PowerShell (Core) 7+ this is no longer a problem - stringification now consistently uses .FullName.

      • However, stringification can still lead to failure for items returned by a PowerShell provider other than the file-system one, such as the registry provider:

        # !! The 2nd command fails, because $regItem stringifies to
        # !! "HKEY_CURRENT_USER\Console" (the registry-native path),
        # !! which Get-Item then misinterprets as a relative file-system path.
        $regItem = Get-Item HKCU:\Console
        Get-Item $regItem
        

The upshot:

For target cmdlets that have -Path / -LiteralPath parameters, provide arguments that are provider items, such as output by Get-ChildItem and Get-Item:

  • preferably via the pipeline:

    # Robust, in both PowerShell editions.
    Get-ChildItem C:\temp\junk -file | Remove-Item
    
  • If you must use an argument, target -LiteralPath explicitly, and use the .PSPath property:

    # Robust, in both PowerShell editions.
    $d = Get-ChildItem C:\temp\junk -file
    Remove-Item -LiteralPath $d.PSPath
    

If, by contrast, you need to pass the paths of provider items to .NET APIs or external programs:

  • Since .PSPath values contain a PowerShell provider prefix (e.g., Microsoft.PowerShell.Core\FileSystem::) in front of the item's native path, such values aren't directly understood by outside parties.

  • The robust approach is to pipe a provider item to Convert-Path in order to convert it to a native path in isolation (piping again results in robust .PSPath binding to -LiteralPath):

     $d | Convert-Path # -> 'C:\temp\junk\delete-me'
    
    • This also works with path strings that are based on PowerShell-only drives, e.g:

      Convert-Path -LiteralPath HKCU:\Console # -> 'HKEY_CURRENT_USER\Console'
      

[1] For most .NET types, PowerShell indeed calls .ToString(), but requests culture-invariant formatting, where supported, via the IFormattable interface; for select types, notably arrays and [pscustomobject], it uses custom stringification in lieu of calling .ToString() - see this answer for details.

mklement0
  • 382,024
  • 64
  • 607
  • 775
2

PowerShell can only do so much for you, and one thing it can't do, is guess what you're after all the time. The issue you're having comes from the parameter binding going on behind the scenes. So, the binding with Remove-Item $g only happens to the Name property from the [FileInfo] object passed to it, which will coerce the object into a string and then tie it to the current working directory binding it to Path.

Whereas, $g | Remove-Item, the binding from the pipeline is bound using a different procedure and takes the PSPath instance member and binds it to the LiteralPath which is the FullName property in your [FileInfo] object.

  • Still coerces the object into an object of [string], just using a different property.
  • Sauce for an okay understanding of ParameterBinding.
    • Using Trace-Command, you can get a much better understanding of this.

Long story short, use $g.FullName for both scenarios as it will output an object of [string], and both Path parameters accept the entirety of the value, by pipeline as well.

Abraham Zinala
  • 4,267
  • 3
  • 9
  • 24
  • Some great pointers, but let me clarify re argument-based binding: It isn't technically, the `.Name` property that binds, it is the result of a `.ToString()` call, which - in Windows PowerShell only - _situationally_ reports either the value of `.Name` or `.FullName` in `Get-ChildItem` output, _depending on the specifics of the call_. `$g.FullName` (or, to support all providers, `$g.PSPath`) should only be used as an _argument_ with `-LiteralPath`. If you use it via the _pipeline_, it binds to `-Path`, risking unintended interpretation as wildcards with paths that contain `[` – mklement0 May 05 '22 at 14:09
  • (Finally, a quibble: `.PSPath` isn't just the `.FullName` value, it is that value (more generally, the provider-native path value) _with a PS provider prefix_, e.g. `Microsoft.PowerShell.Core\FileSystem::C:\foo\bar.txt`) – mklement0 May 05 '22 at 14:10
  • 1
    @mklement0, just reiterating what I saw through `Trace-Command -Name (Get-TraceSource).Name -Expression { Remove-Item $g }`. Didn't want to do a long post and include everything lol also, didn't realize you could use `$g.PSPath` instead. Cool indeed – Abraham Zinala May 05 '22 at 14:19