2

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?

Shayki Abramczyk
  • 36,824
  • 16
  • 89
  • 114
Alex Gordon
  • 57,446
  • 287
  • 670
  • 1,062

3 Answers3

2

You need to provide the file path (FullName):

$releaseNote = Get-Content $latestReleaseNotesFile.FullName
Shayki Abramczyk
  • 36,824
  • 16
  • 89
  • 114
2

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.

vonPryz
  • 22,996
  • 7
  • 54
  • 65
2

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775