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?
CodePudding user response:
You need to provide the file path (FullName
):
$releaseNote = Get-Content $latestReleaseNotesFile.FullName
CodePudding user response:
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.
CodePudding user response:
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 theSystem.IO.FileInfo
instances output byGet-ChildItem
to create[pscustomobject]
instances with only the specified properties, the resulting object can as a whole be piped toGet-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
(andSystem.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
orSystem.IO.DirectoryInfo
instance, explicitly use the.FullName
(file-system-native path) or.PSPath
(includes a PowerShell provider prefix) property 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
andSystem.IO.DirectoryInfo
instances, such as emitted byGet-ChildItem
andGet-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
toGet-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.