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.