TextTable.psm1
[string]$TruncationCharSequence1 = [char]0x393 # Gamma [string]$TruncationCharSequence2 = [char]0xC7 # C-Cedilla [string]$TruncationCharSequence3 = [char]0xAA # Feminine ordinal indicator [string]$TruncationCharSequence = "$TruncationCharSequence1$TruncationCharSequence2$TruncationCharSequence3" [string]$TruncationChar = [char]0x2026 # Represents the ellipsis character [string]$TruncationRegex = "($TruncationChar|$TruncationCharSequence)" <# .DESCRIPTION Parse a text-based table for attributes that characterize it. .OUTPUTS [PSCustomObject] Metadata for the table. #> function Get-TextTableInfo { [CmdletBinding()] param ( # The text-based table to extract information about. [Parameter()] [string[]]$Text, # A regular expression to determine where the column header ends and # table data begins. If null, empty, or whitespace, the first line will # be treated as the header row, and subsequent lines will be the table # data. The default regex requires at least two dashes for the row # separator. This is to accomodate tools that cycle through '-', '\', # '|', and '/' as a busy sequence. [Parameter()] [string]$HeaderRowSeparatorRegEx = '^\s*\-\s*\-+[\s\-]*$', # May be optionally set to indicate that no header separator exists. # This is automatically set if $HeaderRowSeparatorRegEx is null, empty, # or whitespace. [Parameter()] [switch]$NoHeaderSeparator, # A regular expression to determine the last line of the table. # By default, this is the first empty line in the output. [Parameter()] [string]$LastLineRegEx = '^\s*$' ) if ($NoHeaderSeparator -or [string]::IsNullOrWhiteSpace($HeaderRowSeparatorRegEx)) { $NoHeaderSeparator = $true $separatorMatch = $Text | Select-String -Pattern $Text[0] } else { $separatorMatch = @($Text | Select-String -Pattern $HeaderRowSeparatorRegEx) if ($separatorMatch.Count -eq 0) { return $null } } # Only use first match $separatorMatch = $separatorMatch[0] $terminatorMatch = @($Text[$separatorMatch.LineNumber..($Text.Count-1)] | Select-String -Pattern $LastLineRegEx) if ($terminatorMatch.Count -eq 0) { $rowCount = $Text.Count - $separatorMatch.LineNumber } else { $rowCount = $terminatorMatch[0].LineNumber - 2 } # Compute the column width by determining the offset between the first character of adjecent header names. if ($NoHeaderSeparator) { $headerRow = $separatorMatch.LineNumber - 1 } else { $headerRow = $separatorMatch.LineNumber - 2 } $headerNames = $Text[$headerRow] | Select-String '[a-zA-Z]+' -AllMatches $tableInfo = [PSCustomObject]@{ FirstRow = $separatorMatch.LineNumber LastRow = $separatorMatch.LineNumber + $rowCount RowCount = $rowCount ColumnInfo = $null } $columnMetadata = @() for ($columnIndexNumber = 0; $columnIndexNumber -lt $headerNames.Matches.Count; $columnIndexNumber++) { if ($columnIndexNumber -eq ($headerNames.Matches.Count - 1)) { if ($NoHeaderSeparator) { # In this case, the column's width should be based on the max line length of all the rows $maxLength = 0; @($Text[$separatorMatch.LineNumber..($separatorMatch.LineNumber + $rowCount - 1)]) | ForEach-Object { $maxLength = [Math]::Max($maxLength, $_.Length) } $columnWidth = $maxLength - $headerNames.Matches[$columnIndexNumber].Index } else { # NOTE: This may need similar logic used in $NoHeaderSeparator above. This would mainly be # for tables where the separator row does not span the whole line and instead only underlines # the column header name. This is the case for powershell-like tables (via Format-Table). $columnWidth = $Text[$headerRow].Length - $headerNames.Matches[$columnIndexNumber].Index } } else { $columnWidth = $headerNames.Matches[$columnIndexNumber + 1].Index - $headerNames.Matches[$columnIndexNumber].Index } $columnItem = [PSCustomObject]@{ Name = $headerNames.Matches[$columnIndexNumber].Value StartIndex = $headerNames.Matches[$columnIndexNumber].Index EndIndex = ($headerNames.Matches[$columnIndexNumber].Index + $columnWidth - 1) Width = $columnWidth } $columnMetadata += $columnItem } $tableInfo.ColumnInfo = $columnMetadata return $tableInfo } <# .DESCRIPTION Returns a PSCustomObject table entry based on table metadata. #> function ConvertFrom-TextTableItem { param ( # The table metadata. [PSCustomObject]$TableInfo, # The entry to convert. [string]$ItemText, # When set, additional properties will be created to indicate whether # a value contains a trialing ellipsis, indicating the value is # truncated. [switch]$ReportTrucated ) $Entry = $ItemText.Replace($TruncationCharSequence, $TruncationChar) $item = New-Object PSObject $columnCount = 0 foreach ($columnInfo in $TableInfo.ColumnInfo) { $columnCount++ try { if ($columnInfo.StartIndex + $columnInfo.Width -gt $Entry.Length) { $entryField = $Entry.Substring($columnInfo.StartIndex).Trim() } elseif ($columnCount -ge $TableInfo.ColumnInfo.Count) { $entryField = $Entry.Substring($columnInfo.StartIndex).Trim() } else { $entryField = $Entry.Substring($columnInfo.StartIndex, $columnInfo.Width).Trim() } } catch { $entryField = "" } if ($ReportTrucated) { $item | Add-Member -Type NoteProperty -Name "$($columnInfo.Name)Truncated" -Value ($entryField -match $TruncationRegex) } $item | Add-Member -Type NoteProperty -Name $columnInfo.Name -Value $entryField } return $item } <# .DESCRIPTION Converts a text-based table into a PSCustomObject[] based on the detected (column) metadata from the table. NOTE: In the case where multiple tables in the output exists, only the first table will be processed. #> function ConvertFrom-TextTable { [CmdletBinding(PositionalBinding)] param ( # The text-based table to convert to a PSCustomObject. [Parameter(ValueFromPipeline)] [string[]]$Text, # May be optionally set to indicate that no header separator exists. # This is automatically set if $HeaderRowSeparatorRegEx is null, empty, # or whitespace. [Parameter()] [switch]$NoHeaderSeparator, # Contains metadata describing the structure of the table. # Use Get-TextTableInfo for PSCustomObject. [Parameter()] [PSCustomObject]$TableInfo, # A regular expression to determine the last line of the table. # By default, this is the first empty line in the output. [Parameter()] [string]$LastLineRegEx = '^\s*$' ) begin { if ($null -eq $TableInfo) { $textRows = @() } else { $rowIndex = 0; } } process { if ($null -eq $TableInfo) { $textRows += $Text } else { if ($rowIndex -ge $TextInfo.FirstRow -and $rowIndex -ge $TextInfo.LastRow) { ConvertFrom-TextTableItem -TableInfo $TextInfo -ItemText $Text } $rowIndex++ } } end { if ($null -eq $TableInfo) { $TextInfo = Get-TextTableInfo -Text $textRows -LastLineRegEx $LastLineRegEx -NoHeaderSeparator:$NoHeaderSeparator $textRows[$TextInfo.FirstRow..$TextInfo.LastRow] | ForEach-Object { ConvertFrom-TextTableItem -TableInfo $TextInfo -ItemText $_ } } } } |