Home > Software design >  unable to read contents of file
unable to read contents of file

Time:11-15

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 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 (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 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.

  • Related