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>
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.
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.
Related
I'm more familiar with Bash than with Powershell and sometimes I get confused by the latter's object model.
Looking at the documentation of Get-FileHash, it seems that there are 3 ways of specifying the input:
Get-FileHash [-Path]
Get-FileHash [-LiteralPath]
Get-FileHash [-InputStream]
The first two take file names, the third a stream of data.
Now, Get-ChildItem -File seems to output System.IO.FileInfo objects, judging from what Get-Member says:
$ Get-ChildItem -File | Get-Member
TypeName: System.IO.FileInfo
And yet the pipeline Get-ChildItem -File | Get-FileHash works correctly. My question is, what is the mechanism that allows converting System.IO.FileInfo to the type of inputs expected by Get-FileHash?
The System.IO.FileInfo / System.IO.DirectoryInfo instances output by PowerShell cmdlets have a .PSPath property[*] that contains the instances' fully qualified path, which is the full file-system path prefixed by the PS provider name (e.g., Microsoft.PowerShell.Core\FileSystem::C:\windows).
File-processing cmdlets such as Get-FileHash have a -LiteralPath parameter which has an alias name of -PSPath.
Because a -LiteralPath parameter (typically) accepts input from the pipeline by property name, input objects that have a .PSPath property automatically bind to it, by virtue of the PSPath parameter alias name.
As an aside:
File-processing cmdlets also have a -Path parameter, which interprets its arguments as wildcard expressions, not as literal paths.
When you pipe path strings to such cmdlets, they bind to -Path, which notably means that they are indeed interpreted as wildcards - while this will typically not matter, because most literal paths do not contain wildcard metacharacters, it does with paths that contain [ and ], which are then misinterpreted; avoiding this misinterpretation requires escaping them as `[ and `], as shown in this answer.
Due to a bug in Windows PowerShell (since fixed in PowerShell (Core) 7+), Get-FileHash, specifically, doesn't accept strings via the pipeline - see this answer for details.
How to discover this behavior:
Via the online help topic:
Programmatically:
Note: Get-Help Get-FileHash -Parameter LiteralPath | Select-Object name, aliases, pipelineinput works too in this case, but this approach is generally restricted to target commands that come with MAML-based help files, and even those that do can have help files that are out of sync with the actual command definition.
& {
Get-Command $args[0] | % Parameters | % $args[1] |
Select-Object Name, Aliases, #{
n = 'Accepts pipeline input';
e = { $(if ($_.Attributes.ValueFromPipeline) { 'by value' }), $(if ($_.Attributes.ValueFromPipelineByPropertyName) { 'by property name' }) -join ', ' -replace '^, ' }
}
} Get-FileHash LiteralPath
Output:
Name Aliases Accepts pipeline input
---- ------- ----------------------
LiteralPath {PSPath, LP} by property name
[*] It is PowerShell's file-system provider that adds this property, among others. All PowerShell providers decorate their output items this way, such as the Microsoft.Win32.RegistryKey instances output by the registry provider. The underlying .NET types do not have it. See this answer for more information.
From the About Functions Advanced Parameters documentation, ValueFromPipelineByPropertyName argument section:
The ValueFromPipelineByPropertyName argument indicates that the
parameter accepts input from a property of a pipeline object. The
object property must have the same name or alias as the parameter.
For example, if the function has a ComputerName parameter, and the
piped object has a ComputerName property, the value of the
ComputerName property is assigned to the function's ComputerName
parameter.
Update: Originally linked the wrong source code file. The correct Get-FileHash source code is here. As #mklement0 correctly answered the Get-ChildItem cmdlet outputs an object with a PSPath property, which makes this work.
It looks like it uses the pspath property.
dir there | select pspath | get-filehash
Algorithm Hash Path
--------- ---- ----
SHA256 44723DD4D0E0D46A3C7FA8ACA254B61C27B6B5789F96177E82C80700409F1535 C:\users\...
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> Function CustomFunction{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo]$inputFile
)
$inputFile.BaseName
$inputFile.DirectoryName
$inputFile.GetType()
"`n`n"
}
PS C:\tmp> (Get-ChildItem -LiteralPath $PWD -File).ForEach({CustomFunction $_})
In PowerShell (Core) - The type System.IO.FileSystemInfo and the function would work even if the file itself isn't located in the working directory
Another File
C:\tmp
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False FileInfo System.IO.FileSystemInfo
Windows PowerShell is still using a type of System.IO.FileSystemInfo - but there is definitely something different between them. I'm not sure what "IsSerial" actually checks, but if CustomFunction were taking action on the files then it won't work if they're not in the working directory.
Another File
C:\tmp
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
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
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) - this has since been fixed in PowerShell (Core) 7+ (via an underlying .NET API change), where such instances now consistently stringify to their full path.
To make code work robustly in both PowerShell editions, two fundamental scenarios apply:
Passing [System.IO.FileInfo] instances to other file-processing cmdlets, in which case you can provide them as pipeline input - see the next section.
Passing them to external programs and .NET methods, in which case their .FullName property must be used - see the bottom section.
When you need to pass Get-ChildItem / Get-Item to other file-processing cmdlets, namely cmdlets that have pipeline-binding -LiteralPath parameters, such as Get-Item and Get-ChildItem you can stick with [System.IO.FileInfo] instances and provide them as pipeline input ; e.g.:
# Robust alternative to the following *brittle* commands:
# Get-ChildItem $someFileInfoInstance # -Path implied
# Get-ChildItem -LiteralPath $someFileInfoInstance
# Same as:
# Get-ChildItem -LiteralPath $someFileInfoInstance.PSPath
$someFileInfoInstance | Get-ChildItem
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, even in Windows PowerShell.
The reason that it is the .PSPath property value that binds is that -PSPath is defined as an alias of the -LiteralPath parameter, combined with declaring the latter as ValueFromPipelineByPropertyName.
Note:
Generally, all standard cmdlets that have a -LiteralPath parameter support this mechanism.
See the bottom section of this answer for how to infer whether the mechanism is supported by a given cmdlet by examining its parameter definitions.
An unfortunate exception due to its buggy implementation in Windows PowerShell is Import-Csv - again, this has since been fixed in PowerShell (Core) 7+.
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.
That is, you would expect Get-ChildItem -LiteralPath $someFileInfoInstance to bind the parameter, which is string-typed, in the same way as $someFileInfoInstance | Get-ChildItem.
However, that is not the case: when passed as an argument, simple .ToString() stringification is performed, which in combination with Windows PowerShell's inconsistent stringification of System.IO.FileInfo instances causes situational malfunctioning.
Alternatively, you can always explicitly use the .PSPath property value to predictably get a full path, but that is both more verbose and error-prone (easy to forget).
Get-ChildItem -LiteralPath $someFileInfoInstance.PSPath
Note that .PSPath values are prefixed by the name of the PowerShell provider underlying the item at hand, such as Microsoft.PowerShell.Core\FileSystem:: for file-system items.
As such, .PSPath isn't suitable for passing for passing file-system paths to external programs or .NET methods, which are discussed next.
Passing file-system paths to external programs and .NET methods:
In these cases, a full, file-system-native path must be passed.
External programs and .NET methods are unaware of PowerShell-only drives (those established with New-PSDrive), so any paths based on them wouldn't be recognized (and neither would PowerShell paths referring to non-file-system items, such as registry keys based on PowerShell's HKLM: drive).
In the case of external programs, a relative file-system path would work too, but .NET methods require a full path, due to .NET's working directory usually differing from PowerShell's (see this answer).
To ensure use of a full, file-system-native path:
For [System.IO.FileInfo] / [System.IO.DirectoryInfo] instances, use their .FullName property.
As stated above, this is no longer necessary in PowerShell (Core) v6.1+, where such instances consistently stringify to their .FullName property values. (Stringification happens invariably when passing arguments to external programs, and when passing arguments to string-typed (file-path) parameters of .NET methods).
For [System.Management.Automation.PathInfo] instances, such as returned by Get-Location and reflected in the automatic $PWD variable variable, use their .ProviderPath property.
Examples:
cmd /c echo (Get-Item .).FullName
cmd /c echo $PWD.ProviderPath
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.
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.
I'm more familiar with Bash than with Powershell and sometimes I get confused by the latter's object model.
Looking at the documentation of Get-FileHash, it seems that there are 3 ways of specifying the input:
Get-FileHash [-Path]
Get-FileHash [-LiteralPath]
Get-FileHash [-InputStream]
The first two take file names, the third a stream of data.
Now, Get-ChildItem -File seems to output System.IO.FileInfo objects, judging from what Get-Member says:
$ Get-ChildItem -File | Get-Member
TypeName: System.IO.FileInfo
And yet the pipeline Get-ChildItem -File | Get-FileHash works correctly. My question is, what is the mechanism that allows converting System.IO.FileInfo to the type of inputs expected by Get-FileHash?
The System.IO.FileInfo / System.IO.DirectoryInfo instances output by PowerShell cmdlets have a .PSPath property[*] that contains the instances' fully qualified path, which is the full file-system path prefixed by the PS provider name (e.g., Microsoft.PowerShell.Core\FileSystem::C:\windows).
File-processing cmdlets such as Get-FileHash have a -LiteralPath parameter which has an alias name of -PSPath.
Because a -LiteralPath parameter (typically) accepts input from the pipeline by property name, input objects that have a .PSPath property automatically bind to it, by virtue of the PSPath parameter alias name.
As an aside:
File-processing cmdlets also have a -Path parameter, which interprets its arguments as wildcard expressions, not as literal paths.
When you pipe path strings to such cmdlets, they bind to -Path, which notably means that they are indeed interpreted as wildcards - while this will typically not matter, because most literal paths do not contain wildcard metacharacters, it does with paths that contain [ and ], which are then misinterpreted; avoiding this misinterpretation requires escaping them as `[ and `], as shown in this answer.
Due to a bug in Windows PowerShell (since fixed in PowerShell (Core) 7+), Get-FileHash, specifically, doesn't accept strings via the pipeline - see this answer for details.
How to discover this behavior:
Via the online help topic:
Programmatically:
Note: Get-Help Get-FileHash -Parameter LiteralPath | Select-Object name, aliases, pipelineinput works too in this case, but this approach is generally restricted to target commands that come with MAML-based help files, and even those that do can have help files that are out of sync with the actual command definition.
& {
Get-Command $args[0] | % Parameters | % $args[1] |
Select-Object Name, Aliases, #{
n = 'Accepts pipeline input';
e = { $(if ($_.Attributes.ValueFromPipeline) { 'by value' }), $(if ($_.Attributes.ValueFromPipelineByPropertyName) { 'by property name' }) -join ', ' -replace '^, ' }
}
} Get-FileHash LiteralPath
Output:
Name Aliases Accepts pipeline input
---- ------- ----------------------
LiteralPath {PSPath, LP} by property name
[*] It is PowerShell's file-system provider that adds this property, among others. All PowerShell providers decorate their output items this way, such as the Microsoft.Win32.RegistryKey instances output by the registry provider. The underlying .NET types do not have it. See this answer for more information.
From the About Functions Advanced Parameters documentation, ValueFromPipelineByPropertyName argument section:
The ValueFromPipelineByPropertyName argument indicates that the
parameter accepts input from a property of a pipeline object. The
object property must have the same name or alias as the parameter.
For example, if the function has a ComputerName parameter, and the
piped object has a ComputerName property, the value of the
ComputerName property is assigned to the function's ComputerName
parameter.
Update: Originally linked the wrong source code file. The correct Get-FileHash source code is here. As #mklement0 correctly answered the Get-ChildItem cmdlet outputs an object with a PSPath property, which makes this work.
It looks like it uses the pspath property.
dir there | select pspath | get-filehash
Algorithm Hash Path
--------- ---- ----
SHA256 44723DD4D0E0D46A3C7FA8ACA254B61C27B6B5789F96177E82C80700409F1535 C:\users\...
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