functions/data/Get-PSFFileContent.ps1
function Get-PSFFileContent { <# .SYNOPSIS Read the contents of a file. .DESCRIPTION Read the contents of a file. This is a replacement for Get-Content, trading flexibility in return for focus. Notably, it allows for consistent parameterization between PowerShell versions. .PARAMETER Path Path to the file(s) to read. .PARAMETER LiteralPath Literal Path to the file(s) to read. This does NOT use wildcard expressions when evaluating paths. .PARAMETER ReadCount How man lines to include in a single return / dataset. Set to 0 or less to return the entire file as a single string. Example: When reading a file with 13 lines of text, setting "ReadCount" to 5 will return 3 strings: 2 strings of 5 lines each, one with the remaining 3. Defaults to: 1 .PARAMETER TotalCount How many datasets in total to return. When specified, this allows limiting the returned amount of results. This parameter takes ReadCount into account, not counting individual lines of text, but number of result-sets after considering ReadCount. Example 1: File: 50 Lines, ReadCount: 1, TotalCount: 20 In this case, the first 20 lines of the text file are returned Example 2: File: 50 Lines, ReadCount: 3, TotalCount: 10 In this case, the first 10 results of 3-line strings are returned (resulting in 10 strings, covering a total of 30 lines of the text file). .PARAMETER Skip How many dataset to skip. The size of a dataset depends on the ReadCount parameter. Skips the first X datasets, unless combined with "Last", in which case it will skip the last X datasets instead. .PARAMETER Last Only return the last X datasets from the file. The size of a dataset depends on the ReadCount parameter. .PARAMETER AsByteStream Return the content of the file as a binary stream. This parameter changes the behavior of many other parameters, as it is not compatible with "-ReadCount". Each dataset is now always a single byte. Example: TotalCount: 48, Skip: 12 In this example, it will skip the first 12 bytes in the file and then return the next 48 (or less, if there are fewer bytes in the file in total). .PARAMETER Wait Rather than execute the command and then return, wait for some time and keep looking for new entries to be added to the file. This will return new lines / datasets as they are added to the file. This command will look every second for new content in the file. .PARAMETER Timeout The amount of time to wait before stopping waiting for new content in the file. Defaults to: 1 hour. .PARAMETER Encoding The encoding to interpret the text file under. Has no effect when using "-AsByteStream". Defaults to: UTF8 (with BOM). .EXAMPLE PS C:\> Get-PSFFileContent .\response.json | ConvertFrom-Json Read a json file and convert into useful objects. .EXAMPLE PS C:\> Get-PSFFileContent .\service-2025-08-14.log -Last 20 -Wait Read the last 20 lines in the specified logfile, then wait for more lines as they are written to the file. .EXAMPLE PS C:\> $certBytes = Get-PSFFileContent -Path .\cert.cer -AsByteStream Reads the bytes from the specified certificate file. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding(DefaultParameterSetName = 'Text')] param ( [Parameter(Position = 0, ValueFromPipeline = $true)] [PSFFile] $Path, [PSFLiteralPath] $LiteralPath, [Parameter(ParameterSetName = 'Text')] [long] $ReadCount = 1, [long] $TotalCount, [long] $Skip, [long] $Last, [Parameter(Mandatory = $true, ParameterSetName = 'Bytes')] [switch] $AsByteStream, [switch] $Wait, [PSFTimeSpan] $Timeout = '1h', [Parameter(ParameterSetName = 'Text')] [PSFArgumentCompleter('PSFramework-Encoding')] [PSFEncoding] $Encoding = 'UTF8' ) begin { #region Utility Functions function Read-Stream { [CmdletBinding()] param ( [System.IO.StreamReader] $Reader, [int] $ReadCount, [int] $Count, [switch] $All, [hashtable] $Counter = @{ Total = 0 } ) $currentCount = 0 while ( -not $Reader.EndOfStream -and ( ($Count -lt 1) -or ($currentCount -lt $Count) ) -and ( (-not $Counter.Limit) -or ($Counter.Limit -lt 1) -or ($Counter.Limit -gt $Counter.Total) ) ) { $lines = foreach ($index in 1..$ReadCount) { if (-not $Reader.EndOfStream) { $Reader.ReadLine() } } $lines -join "`n" $currentCount++ $Counter.Total++ if (-not $All -and $PSBoundParameters.Keys -notcontains 'Count') { break } } } #endregion Utility Functions $first = $true } process { if ($first) { $files = $Path + $LiteralPath | Remove-PSFNull $first = $false } else { $files = $Path } :main foreach ($filePath in $files) { #region Binary Read if ($AsByteStream) { try { $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, ([System.IO.FileShare]'ReadWrite, Delete')) } catch { $PSCmdlet.WriteError($_) continue } $start = 0 $end = $fileStream.Length if ($PSBoundParameters.Keys -contains 'Last') { $start = $end - $Last if ($Skip -gt 0) { $start = $start - $Skip $end = $end - $Skip } } elseif ($PSBoundParameters.Keys -contains 'TotalCount') { $end = $start + $TotalCount if ($Skip -gt 0) { $start = $start + $Skip $end = $end + $Skip } } if ($start -lt 0) { $start = 0 } if ($end -gt $fileStream.Length) { $end = $fileStream.Length } if ($start -eq $end) { continue } $length = $end - $start $buffer = [byte[]]::new($length) try { $fileStream.Position = $Start $null = $fileStream.Read($buffer, 0, $Length) , $buffer } catch { $PSCmdlet.WriteError($_) } finally { $fileStream.Close() $fileStream.Dispose() } continue } #endregion Binary Read #region Text Read try { $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, ([System.IO.FileShare]'ReadWrite, Delete')) } catch { $PSCmdlet.WriteError($_) continue } $reader = [System.IO.StreamReader]::new($fileStream, $Encoding) try { if ($PSBoundParameters.Keys -contains 'Last') { $lines = [PSFramework.Utility.LimitedConcurrentQueue[string]]::new(($Last + $Skip)) foreach ($result in Read-Stream -Reader $reader -ReadCount $ReadCount -All) { $lines.Enqueue($result) } $total = $Last - ($lines.Size - $lines.Count) # We skipped more than we had if ($total -lt 1) { continue main } @($($lines))[0..($total - 1)] } if ($Skip -gt 1) { $null = Read-Stream -Reader $reader -ReadCount $ReadCount -Count $Skip } if ($reader.EndOfStream -and -not $Wait) { continue main } $counter = @{ Total = 0 Limit = $TotalCount } if (-not $reader.EndOfStream) { if ($ReadCount -lt 1) { $reader.ReadToEnd().Trim("`n`r") if (-not $Wait) { continue main } } else { Read-Stream -Reader $reader -ReadCount $ReadCount -All -Counter $counter } } if (-not $Wait) { continue main } $start = Get-Date $timeLimit = $start.Add($Timeout) while ( ( ($TotalCount -le 0) -or ($counter.Total -lt $counter.Limit) ) -and (([datetime]::Now) -lt $timeLimit) ) { if (-not $reader.EndOfStream) { Read-Stream -Reader $reader -ReadCount $ReadCount -All -Counter $counter } Start-Sleep -Seconds 1 } } catch { $PSCmdlet.WriteError($_) } finally { $fileStream.Close() $fileStream.Dispose() } #endregion Text Read } } } |