From 58b3e69ac98f897472e0ef620b6d2e824396dbe5 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Wed, 25 Sep 2024 23:08:43 -0400 Subject: [PATCH 01/15] Add PSStyle support Closes #1 --- source/ErrorView.format.ps1xml | 38 +++++++++++++++++++ source/public/ConvertTo-CategoryErrorView.ps1 | 9 ++++- source/public/ConvertTo-DetailedErrorView.ps1 | 6 +-- source/public/ConvertTo-FullErrorView.ps1 | 28 +++++++++++--- source/public/ConvertTo-NormalErrorView.ps1 | 15 ++++++-- .../public/ConvertTo-NormalExceptionView.ps1 | 26 +++++++++++++ source/public/ConvertTo-SimpleErrorView.ps1 | 13 ++++++- source/public/Format-Error.ps1 | 15 ++++---- source/public/Set-ErrorView.ps1 | 3 +- source/public/Write-NativeCommandError.ps1 | 36 +++++++++++------- 10 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 source/public/ConvertTo-NormalExceptionView.ps1 diff --git a/source/ErrorView.format.ps1xml b/source/ErrorView.format.ps1xml index 4b3a2c7..443e23a 100644 --- a/source/ErrorView.format.ps1xml +++ b/source/ErrorView.format.ps1xml @@ -30,5 +30,43 @@ </CustomEntries> </CustomControl> </View> + <View> + <Name>ExceptionInstance</Name> + <OutOfBand /> + <ViewSelectedBy> + <TypeName>System.Exception</TypeName> + </ViewSelectedBy> + <CustomControl> + <CustomEntries> + <CustomEntry> + <CustomItem> + <ExpressionBinding> + <ScriptBlock>Write-NativeCommandError $_</ScriptBlock> + </ExpressionBinding> + <ExpressionBinding> + <ScriptBlock> + <![CDATA[ + if ($_.ErrorRecord) { + $Record = $_.ErrorRecord + if ($formatter = @(Get-Command "ConvertTo-$($ErrorView -replace "View$")ErrorView" -ListImported -ErrorAction Ignore -ParameterName InputObject -ParameterType [System.Management.Automation.ErrorRecord])) { + & ($formatter[0]) -InputObject $Record + } else { + ConvertTo-NormalErrorView $Record + } + } else { + if ($formatter = @(Get-Command "ConvertTo-$($ErrorView -replace "View$")ExceptionView" -ListImported -ErrorAction Ignore -ParameterName InputObject -ParameterType [System.Exception])) { + & ($formatter[0]) -InputObject $_ + } else { + ConvertTo-NormalExceptionView $_ + } + } + ]]> + </ScriptBlock> + </ExpressionBinding> + </CustomItem> + </CustomEntry> + </CustomEntries> + </CustomControl> + </View> </ViewDefinitions> </Configuration> diff --git a/source/public/ConvertTo-CategoryErrorView.ps1 b/source/public/ConvertTo-CategoryErrorView.ps1 index ccb619a..c5fb25f 100644 --- a/source/public/ConvertTo-CategoryErrorView.ps1 +++ b/source/public/ConvertTo-CategoryErrorView.ps1 @@ -15,5 +15,12 @@ filter ConvertTo-CategoryErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) - $InputObject.CategoryInfo.GetMessage() + $resetColor = '' + $errorColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + } + $errorColor + $InputObject.CategoryInfo.GetMessage() + $resetColor } \ No newline at end of file diff --git a/source/public/ConvertTo-DetailedErrorView.ps1 b/source/public/ConvertTo-DetailedErrorView.ps1 index ed52bcb..fbc1c58 100644 --- a/source/public/ConvertTo-DetailedErrorView.ps1 +++ b/source/public/ConvertTo-DetailedErrorView.ps1 @@ -32,9 +32,9 @@ function ConvertTo-DetailedErrorView { $OutputRoot = [System.Text.StringBuilder]::new() if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - $resetColor = $PSStyle.Reset - $errorColor = $PSStyle.Formatting.Error - $accentColor = $PSStyle.Formatting.FormatAccent + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + $accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } } function DetailedErrorView { diff --git a/source/public/ConvertTo-FullErrorView.ps1 b/source/public/ConvertTo-FullErrorView.ps1 index dbe4ce5..5377ef8 100644 --- a/source/public/ConvertTo-FullErrorView.ps1 +++ b/source/public/ConvertTo-FullErrorView.ps1 @@ -3,7 +3,7 @@ filter ConvertTo-FullErrorView { .SYNOPSIS Converts an ErrorRecord to a full error view .DESCRIPTION - The most verbose error view I've got, it shows everything, recursing forever. + A simple, verbose error view that just shows everything, recursing forever. #> [CmdletBinding()] param( @@ -12,19 +12,35 @@ filter ConvertTo-FullErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) - $PSStyle.OutputRendering, $Rendering = "Ansi", $PSStyle.OutputRendering - $Detail = $InputObject | Format-List * -Force | Out-String - $PSStyle.OutputRendering = $Rendering + $resetColor = '' + $errorColor = '' + #$accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + # For Format-List to use color when piped to Out-String, OutputRendering needs to be Ansi + $PSStyle.OutputRendering, $Rendering = "Ansi", $PSStyle.OutputRendering + + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } + $Detail = $InputObject | Format-List * -Force | Out-String -Width 120 + $Detail = $Detail -replace "((?:Exception|FullyQualifiedErrorId).*`e\[0m)(.*)", "$($PSStyle.Formatting.ErrorAccent)`$1$($PSStyle.Formatting.Error)`$2$($PSStyle.Reset)" + } else { + $Detail = $InputObject | Format-List * -Force | Out-String -Width 120 + } # NOTE: ErrorViewRecurse is normally false, and only set temporarily by Format-Error -Recurse if ($ErrorViewRecurse) { $Count = 1 $Exception = $InputObject.Exception while ($Exception = $Exception.InnerException) { - $Detail += "`nINNER EXCEPTION $($Count): $($Exception.GetType().FullName)`n`n" - $Detail += $Exception | Format-List * -Force | Out-String + $Detail += $errorColor + "`nINNER EXCEPTION $($Count): $resetColor$($Exception.GetType().FullName)`n`n" + $Detail += $Exception | Format-List * -Force | Out-String -Width 120 $Count++ } } + if ($resetColor) { + $PSStyle.OutputRendering = $Rendering + } $Detail } \ No newline at end of file diff --git a/source/public/ConvertTo-NormalErrorView.ps1 b/source/public/ConvertTo-NormalErrorView.ps1 index 182d84e..cdcd820 100644 --- a/source/public/ConvertTo-NormalErrorView.ps1 +++ b/source/public/ConvertTo-NormalErrorView.ps1 @@ -11,9 +11,18 @@ filter ConvertTo-NormalErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) + $resetColor = '' + $errorColor = '' + #$accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } + } if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { - $InputObject.Exception.Message + $errorColor + $InputObject.Exception.Message + $resetColor } else { $myinv = $InputObject.InvocationInfo if ($myinv -and ($myinv.MyCommand -or ($InputObject.CategoryInfo.Category -ne 'ParserError'))) { @@ -66,9 +75,9 @@ filter ConvertTo-NormalErrorView { } if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { - $InputObject.Exception.Message + $posmsg + "`n " + $errorColor + $InputObject.Exception.Message + $posmsg + $resetColor + "`n " } else { - $InputObject.ErrorDetails.Message + $posmsg + $errorColor + $InputObject.ErrorDetails.Message + $posmsg + $resetColor } } } \ No newline at end of file diff --git a/source/public/ConvertTo-NormalExceptionView.ps1 b/source/public/ConvertTo-NormalExceptionView.ps1 new file mode 100644 index 0000000..fea1db0 --- /dev/null +++ b/source/public/ConvertTo-NormalExceptionView.ps1 @@ -0,0 +1,26 @@ +filter ConvertTo-NormalExceptionView { + <# + .SYNOPSIS + Converts an Exception to a NormalView message string + .DESCRIPTION + The original default PowerShell ErrorView, updated for VT100 + #> + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [System.Exception] + $InputObject + ) + $resetColor = '' + $errorColor = '' + #$accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } + } + + $errorColor + $InputObject.Exception.Message + $resetColor + +} \ No newline at end of file diff --git a/source/public/ConvertTo-SimpleErrorView.ps1 b/source/public/ConvertTo-SimpleErrorView.ps1 index 20e4849..97197ff 100644 --- a/source/public/ConvertTo-SimpleErrorView.ps1 +++ b/source/public/ConvertTo-SimpleErrorView.ps1 @@ -4,6 +4,15 @@ function ConvertTo-SimpleErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) + $resetColor = '' + $errorColor = '' + #$accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } + } if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { $InputObject.Exception.Message @@ -46,9 +55,9 @@ function ConvertTo-SimpleErrorView { } if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { - $InputObject.Exception.Message + $posmsg + "`n " + $errorColor + $InputObject.Exception.Message + $posmsg + $resetColor + "`n " } else { - $InputObject.ErrorDetails.Message + $posmsg + $errorColor + $InputObject.ErrorDetails.Message + $posmsg + $resetColor } } } \ No newline at end of file diff --git a/source/public/Format-Error.ps1 b/source/public/Format-Error.ps1 index 59a80c8..2e7edf2 100644 --- a/source/public/Format-Error.ps1 +++ b/source/public/Format-Error.ps1 @@ -33,17 +33,18 @@ function Format-Error { })] $View = "Detailed", - [Parameter(ParameterSetName="Count", Mandatory)] + [Parameter(ParameterSetName="Count")] [int]$Newest = 1, # Error records (e.g. from $Error). Defaults to the most recent error: $Error[0] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="InputObject", Mandatory)] [Alias("ErrorRecord")] - [System.Management.Automation.ErrorRecord]$InputObject = $( - $e = $Error[0..($Newest-1)] - if ($e -is ([System.Management.Automation.ErrorRecord])) { $e } - elseif ($e.ErrorRecord -is ([System.Management.Automation.ErrorRecord])) { $e.ErrorRecord } - elseif ($Error.Count -eq 0) { Write-Warning "The global `$Error collection is empty" } + [PSObject]$InputObject = $( + if ($global:Error.Count -eq 0) { + Write-Warning "The global `$Error collection is empty" + } else { + $global:Error[0..($Newest-1)] + } ), # Allows ErrorView functions to recurse to InnerException @@ -51,7 +52,7 @@ function Format-Error { ) begin { $ErrorActionPreference = "Continue" - $View, $ErrorView = $ErrorView, $View + $ErrorView, $View = $View, $ErrorView [bool]$Recurse, [bool]$ErrorViewRecurse = [bool]$ErrorViewRecurse, $Recurse } process { diff --git a/source/public/Set-ErrorView.ps1 b/source/public/Set-ErrorView.ps1 index cd52ea6..8041415 100644 --- a/source/public/Set-ErrorView.ps1 +++ b/source/public/Set-ErrorView.ps1 @@ -27,7 +27,6 @@ filter Set-ErrorView { ).Name -replace "ConvertTo-(\w+)ErrorView", '$1View' | Select-Object -Unique $ofs = ';' - [ScriptBlock]::Create("enum ErrorView { $Names }").Invoke() - + .([ScriptBlock]::Create("enum ErrorView { $Names }")) [ErrorView]$global:ErrorView = $View } diff --git a/source/public/Write-NativeCommandError.ps1 b/source/public/Write-NativeCommandError.ps1 index 08b699f..85d98d1 100644 --- a/source/public/Write-NativeCommandError.ps1 +++ b/source/public/Write-NativeCommandError.ps1 @@ -1,39 +1,47 @@ function Write-NativeCommandError { [CmdletBinding()] param( - [System.Management.Automation.ErrorRecord] $InputObject ) + $resetColor = '' + $errorColor = '' + $accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = "$([char]0x1b)[0m" + $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + $accentColor = $PSStyle.Formatting.ErrorAccent + } if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { return } - $myinv = $InputObject.InvocationInfo - if ($myinv -and $myinv.MyCommand) { - switch -regex ( $myinv.MyCommand.CommandType ) { + $invoc = $InputObject.InvocationInfo + if ($invoc -and $invoc.MyCommand) { + switch -regex ( $invoc.MyCommand.CommandType ) { ([System.Management.Automation.CommandTypes]::ExternalScript) { - if ($myinv.MyCommand.Path) { - $myinv.MyCommand.Path + " : " + if ($invoc.MyCommand.Path) { + $accentColor + $invoc.MyCommand.Path + " : " + $resetColor } break } ([System.Management.Automation.CommandTypes]::Script) { - if ($myinv.MyCommand.ScriptBlock) { - $myinv.MyCommand.ScriptBlock.ToString() + " : " + if ($invoc.MyCommand.ScriptBlock) { + $accentColor + $invoc.MyCommand.ScriptBlock.ToString() + " : " + $resetColor } break } default { - if ($myinv.InvocationName -match '^[&\.]?$') { - if ($myinv.MyCommand.Name) { - $myinv.MyCommand.Name + " : " + if ($invoc.InvocationName -match '^[&\.]?$') { + if ($invoc.MyCommand.Name) { + $accentColor + $invoc.MyCommand.Name + " : " + $resetColor } } else { - $myinv.InvocationName + " : " + $accentColor + $invoc.InvocationName + " : " + $resetColor } break } } - } elseif ($myinv -and $myinv.InvocationName) { - $myinv.InvocationName + " : " + } elseif ($invoc -and $invoc.InvocationName) { + $accentColor + $invoc.InvocationName + " : " + $resetColor } } \ No newline at end of file From d08e6e8c9a495188ceab43e2e92e61258e450999 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Wed, 25 Sep 2024 23:35:45 -0400 Subject: [PATCH 02/15] Update with views from 7 --- ErrorView.code-workspace | 14 + Reference/default.ps1 | 335 ++++++++++++++++++ Reference/detail copy.ps1 | 183 ++++++++++ Reference/detail.ps1 | 183 ++++++++++ source/ErrorView.format.ps1xml | 30 ++ source/prefix.ps1 | 47 ++- source/private/GetListRecursive.ps1 | 160 +++++++++ source/private/GetYamlRecursive.ps1 | 169 +++++++++ source/private/WrapString.ps1 | 119 +++++++ source/public/ConvertTo-ConciseErrorView.ps1 | 62 ++++ source/public/ConvertTo-DetailedErrorView.ps1 | 183 +--------- source/public/ConvertTo-FullErrorView.ps1 | 7 +- source/public/ConvertTo-NormalErrorView.ps1 | 23 +- .../public/ConvertTo-NormalExceptionView.ps1 | 2 +- source/public/ConvertTo-SimpleErrorView.ps1 | 15 +- source/public/ConvertTo-YamlErrorView.ps1 | 33 ++ source/public/Format-Error.ps1 | 23 +- source/public/Set-ErrorView.ps1 | 3 +- source/public/Write-NativeCommandError.ps1 | 52 +-- 19 files changed, 1394 insertions(+), 249 deletions(-) create mode 100644 ErrorView.code-workspace create mode 100644 Reference/default.ps1 create mode 100644 Reference/detail copy.ps1 create mode 100644 Reference/detail.ps1 create mode 100644 source/private/GetListRecursive.ps1 create mode 100644 source/private/GetYamlRecursive.ps1 create mode 100644 source/private/WrapString.ps1 create mode 100644 source/public/ConvertTo-ConciseErrorView.ps1 create mode 100644 source/public/ConvertTo-YamlErrorView.ps1 diff --git a/ErrorView.code-workspace b/ErrorView.code-workspace new file mode 100644 index 0000000..0f707bf --- /dev/null +++ b/ErrorView.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "name": "ErrorView", + "path": "." + }, + { + "path": "../../Tasks" + } + ], + "settings": { + "powershell.cwd": "." + } +} \ No newline at end of file diff --git a/Reference/default.ps1 b/Reference/default.ps1 new file mode 100644 index 0000000..d4f6ac8 --- /dev/null +++ b/Reference/default.ps1 @@ -0,0 +1,335 @@ +<# +if (@('NativeCommandErrorMessage','NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView','ConciseView','DetailedView') -notcontains $ErrorView) +{ + $myinv = $_.InvocationInfo + if ($myinv -and $myinv.MyCommand) + { + switch -regex ( $myinv.MyCommand.CommandType ) + { + ([System.Management.Automation.CommandTypes]::ExternalScript) + { + if ($myinv.MyCommand.Path) + { + $myinv.MyCommand.Path + ' : ' + } + + break + } + + ([System.Management.Automation.CommandTypes]::Script) + { + if ($myinv.MyCommand.ScriptBlock) + { + $myinv.MyCommand.ScriptBlock.ToString() + ' : ' + } + + break + } + default + { + if ($myinv.InvocationName -match '^[&\.]?$') + { + if ($myinv.MyCommand.Name) + { + $myinv.MyCommand.Name + ' : ' + } + } + else + { + $myinv.InvocationName + ' : ' + } + + break + } + } + } + elseif ($myinv -and $myinv.InvocationName) + { + $myinv.InvocationName + ' : ' + } +} + + +Set-StrictMode -Off +$ErrorActionPreference = 'Stop' +trap { 'Error found in error view definition: ' + $_.Exception.Message } +$newline = [Environment]::Newline + +$resetColor = '' +$errorColor = '' +$accentColor = '' + +if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = $PSStyle.Reset + $errorColor = $PSStyle.Formatting.Error + $accentColor = $PSStyle.Formatting.ErrorAccent +} +function Get-ConciseViewPositionMessage { + + # returns a string cut to last whitespace + function Get-TruncatedString($string, [int]$length) { + + if ($string.Length -le $length) { + return $string + } + + return ($string.Substring(0,$length) -split '\s',-2)[0] + } + + $posmsg = '' + $headerWhitespace = '' + $offsetWhitespace = '' + $message = '' + $prefix = '' + + # The checks here determine if we show line detailed error information: + # - check if `ParserError` and comes from PowerShell which eventually results in a ParseException, but during this execution it's an ErrorRecord + # - check if invocation is a script or multiple lines in the console + # - check that it's not a script module as expectation is that users don't want to see the line of error within a module + if ((($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception -is 'System.Management.Automation.ParentContainsErrorRecordException') -or $myinv.ScriptName -or $myinv.ScriptLineNumber -gt 1) -and $myinv.ScriptName -notmatch '\.psm1$') { + $useTargetObject = $false + + # Handle case where there is a TargetObject and we can show the error at the target rather than the script source + if ($_.TargetObject.Line -and $_.TargetObject.LineText) { + $posmsg = "${resetcolor}$($_.TargetObject.File)${newline}" + $useTargetObject = $true + } + elseif ($myinv.ScriptName) { + if ($env:TERM_PROGRAM -eq 'vscode') { + # If we are running in vscode, we know the file:line:col links are clickable so we use this format + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber):$($myinv.OffsetInLine)${newline}" + } + else { + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber)${newline}" + } + } + else { + $posmsg = "${newline}" + } + + if ($useTargetObject) { + $scriptLineNumber = $_.TargetObject.Line + $scriptLineNumberLength = $_.TargetObject.Line.ToString().Length + } + else { + $scriptLineNumber = $myinv.ScriptLineNumber + $scriptLineNumberLength = $myinv.ScriptLineNumber.ToString().Length + } + + if ($scriptLineNumberLength -gt 4) { + $headerWhitespace = ' ' * ($scriptLineNumberLength - 4) + } + + $lineWhitespace = '' + if ($scriptLineNumberLength -lt 4) { + $lineWhitespace = ' ' * (4 - $scriptLineNumberLength) + } + + $verticalBar = '|' + $posmsg += "${accentColor}${headerWhitespace}Line ${verticalBar}${newline}" + + $highlightLine = '' + if ($useTargetObject) { + $line = $_.TargetObject.LineText.Trim() + $offsetLength = 0 + $offsetInLine = 0 + } + else { + $positionMessage = $myinv.PositionMessage.Split($newline) + $line = $positionMessage[1].Substring(1) # skip the '+' at the start + $highlightLine = $positionMessage[$positionMessage.Count - 1].Substring(1) + $offsetLength = $highlightLine.Trim().Length + $offsetInLine = $highlightLine.IndexOf('~') + } + + if (-not $line.EndsWith($newline)) { + $line += $newline + } + + # don't color the whole line + if ($offsetLength -lt $line.Length - 1) { + $line = $line.Insert($offsetInLine + $offsetLength, $resetColor).Insert($offsetInLine, $accentColor) + } + + $posmsg += "${accentColor}${lineWhitespace}${ScriptLineNumber} ${verticalBar} ${resetcolor}${line}" + $offsetWhitespace = ' ' * $offsetInLine + $prefix = "${accentColor}${headerWhitespace} ${verticalBar} ${errorColor}" + if ($highlightLine -ne '') { + $posMsg += "${prefix}${highlightLine}${newline}" + } + $message = "${prefix}" + } + + if (! $err.ErrorDetails -or ! $err.ErrorDetails.Message) { + if ($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception.Message.Contains("~$newline")) { + # need to parse out the relevant part of the pre-rendered positionmessage + $message += $err.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } + elseif ($err.Exception) { + $message += $err.Exception.Message + } + elseif ($err.Message) { + $message += $err.Message + } + else { + $message += $err.ToString() + } + } + else { + $message += $err.ErrorDetails.Message + } + + # if rendering line information, break up the message if it's wider than the console + if ($myinv -and $myinv.ScriptName -or $err.CategoryInfo.Category -eq 'ParserError') { + $prefixLength = "$([char]27)]8;;{0}`a{1}$([char]27)]8;;`a" -f $pwd, $pwd::new($prefix).ContentLength + $prefixVtLength = $prefix.Length - $prefixLength + + # replace newlines in message so it lines up correct + $message = $message.Replace($newline, ' ').Replace("`n", ' ').Replace("`t", ' ') + + $windowWidth = 120 + if ($Host.UI.RawUI -ne $null) { + $windowWidth = $Host.UI.RawUI.WindowSize.Width + } + + if ($windowWidth -gt 0 -and ($message.Length - $prefixVTLength) -gt $windowWidth) { + $sb = [Text.StringBuilder]::new() + $substring = Get-TruncatedString -string $message -length ($windowWidth + $prefixVTLength) + $null = $sb.Append($substring) + $remainingMessage = $message.Substring($substring.Length).Trim() + $null = $sb.Append($newline) + while (($remainingMessage.Length + $prefixLength) -gt $windowWidth) { + $subMessage = $prefix + $remainingMessage + $substring = Get-TruncatedString -string $subMessage -length ($windowWidth + $prefixVtLength) + + if ($substring.Length - $prefix.Length -gt 0) + { + $null = $sb.Append($substring) + $null = $sb.Append($newline) + $remainingMessage = $remainingMessage.Substring($substring.Length - $prefix.Length).Trim() + } + else + { + break + } + } + $null = $sb.Append($prefix + $remainingMessage.Trim()) + $message = $sb.ToString() + } + + $message += $newline + } + + $posmsg += "${errorColor}" + $message + + $reason = 'Error' + if ($err.Exception -and $err.Exception.WasThrownFromThrowStatement) { + $reason = 'Exception' + } + # MyCommand can be the script block, so we don't want to show that so check if it's an actual command + elseif ($myinv.MyCommand -and $myinv.MyCommand.Name -and (Get-Command -Name $myinv.MyCommand -ErrorAction Ignore)) + { + $reason = $myinv.MyCommand + } + # If it's a scriptblock, better to show the command in the scriptblock that had the error + elseif ($_.CategoryInfo.Activity) { + $reason = $_.CategoryInfo.Activity + } + elseif ($myinv.MyCommand) { + $reason = $myinv.MyCommand + } + elseif ($myinv.InvocationName) { + $reason = $myinv.InvocationName + } + elseif ($err.CategoryInfo.Category) { + $reason = $err.CategoryInfo.Category + } + elseif ($err.CategoryInfo.Reason) { + $reason = $err.CategoryInfo.Reason + } + + $errorMsg = 'Error' + + "${errorColor}${reason}: ${posmsg}${resetcolor}" +} + +$myinv = $_.InvocationInfo +$err = $_ +if (!$myinv -and $_.ErrorRecord -and $_.ErrorRecord.InvocationInfo) { + $err = $_.ErrorRecord + $myinv = $err.InvocationInfo +} + +if ($err.FullyQualifiedErrorId -eq 'NativeCommandErrorMessage' -or $err.FullyQualifiedErrorId -eq 'NativeCommandError') { + return "${errorColor}$($err.Exception.Message)${resetcolor}" +} + +$myinv = $err.InvocationInfo +if ($ErrorView -eq 'DetailedView') { + $message = Get-Error + return "${errorColor}${message}${resetcolor}" +} + +if ($ErrorView -eq 'CategoryView') { + $message = $err.CategoryInfo.GetMessage() + return "${errorColor}${message}${resetcolor}" +} + +$posmsg = '' +if ($ErrorView -eq 'ConciseView') { + $posmsg = Get-ConciseViewPositionMessage +} +elseif ($myinv -and ($myinv.MyCommand -or ($err.CategoryInfo.Category -ne 'ParserError'))) { + $posmsg = $myinv.PositionMessage +} + +if ($posmsg -ne '') { + $posmsg = $newline + $posmsg +} + +if ($err.PSMessageDetails) { + $posmsg = ' : ' + $err.PSMessageDetails + $posmsg +} + +if ($ErrorView -eq 'ConciseView') { + if ($err.PSMessageDetails) { + $posmsg = "${errorColor}${posmsg}" + } + return $posmsg +} + +$indent = 4 + +$errorCategoryMsg = $err.ErrorCategory_Message + +if ($null -ne $errorCategoryMsg) +{ + $indentString = '+ CategoryInfo : ' + $err.ErrorCategory_Message +} +else +{ + $indentString = '+ CategoryInfo : ' + $err.CategoryInfo +} + +$posmsg += $newline + $indentString + +$indentString = "+ FullyQualifiedErrorId : " + $err.FullyQualifiedErrorId +$posmsg += $newline + $indentString + +$originInfo = $err.OriginInfo + +if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) +{ + $indentString = "+ PSComputerName : " + $originInfo.PSComputerName + $posmsg += $newline + $indentString +} + +$finalMsg = if ($err.ErrorDetails.Message) { + $err.ErrorDetails.Message + $posmsg +} else { + $err.Exception.Message + $posmsg +} + +"${errorColor}${finalMsg}${resetcolor}" + +#> \ No newline at end of file diff --git a/Reference/detail copy.ps1 b/Reference/detail copy.ps1 new file mode 100644 index 0000000..79e9c36 --- /dev/null +++ b/Reference/detail copy.ps1 @@ -0,0 +1,183 @@ +<# +[int]$maxDepth = 10 +Set-StrictMode -Off + +$ellipsis = "`u{2026}" +$resetColor = '' +$errorColor = '' +$accentColor = '' + +if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = $PSStyle.Reset + $errorColor = $psstyle.Formatting.Error + $accentColor = $PSStyle.Formatting.FormatAccent +} + +function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) { + $newline = [Environment]::Newline + $output = [System.Text.StringBuilder]::new() + $prefix = ' ' * $indent + + $expandTypes = @( + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) + + # if object is an Exception, add an ExceptionType property + if ($obj -is [Exception]) { + $obj | Add-Member -NotePropertyName Type -NotePropertyValue $obj.GetType().FullName -ErrorAction Ignore + } + + # first find the longest property so we can indent properly + $propLength = 0 + foreach ($prop in $obj.PSObject.Properties) { + if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { + $propLength = $prop.Name.Length + } + } + + $addedProperty = $false + foreach ($prop in $obj.PSObject.Properties) { + + # don't show empty properties or our added property for $error[index] + if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { + $addedProperty = $true + $null = $output.Append($prefix) + $null = $output.Append($accentColor) + $null = $output.Append($prop.Name) + $propNameIndent = ' ' * ($propLength - $prop.Name.Length) + $null = $output.Append($propNameIndent) + $null = $output.Append(' : ') + $null = $output.Append($resetColor) + + $newIndent = $indent + 4 + + # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes + if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or + $expandTypes -contains $prop.TypeNameOfValue -or ($prop.TypeNames -ne $null -and $expandTypes -contains $prop.TypeNames[0])) { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $null = $output.Append($newline) + $null = $output.Append((Show-ErrorRecord $prop.Value $newIndent ($depth + 1))) + } + } + # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members + elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $targetSite = [PSCustomObject]@{ + Name = $prop.Value.Name + DeclaringType = $prop.Value.DeclaringType + MemberType = $prop.Value.MemberType + Module = $prop.Value.Module + } + + $null = $output.Append($newline) + $null = $output.Append((Show-ErrorRecord $targetSite $newIndent ($depth + 1))) + } + } + # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation + elseif ($prop.Name -eq 'StackTrace') { + # for a stacktrace which is usually quite wide with info, we left justify it + $null = $output.Append($newline) + $null = $output.Append($prop.Value) + } + # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here + elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { + $isFirstElement = $true + foreach ($key in $prop.Value.Keys) { + if ($isFirstElement) { + $null = $output.Append($newline) + } + + if ($key -eq 'Authorization') { + $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}${ellipsis}${newline}") + } + else { + $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}$($prop.Value[$key])${newline}") + } + + $isFirstElement = $false + } + } + # if the object implements IEnumerable and not a string, we try to show each object + # We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here + elseif (!($prop.Value -is [System.String]) -and $prop.Value.GetType().GetInterface('IEnumerable') -ne $null -and $prop.Name -ne 'Data') { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $isFirstElement = $true + foreach ($value in $prop.Value) { + $null = $output.Append($newline) + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1))) + $isFirstElement = $false + } + } + } + # Anything else, we convert to string. + # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error + else { + $value = $null + if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $value -ne $null) + { + if ($prop.Name -eq 'PositionMessage') { + $value = $value.Insert($value.IndexOf('~'), $errorColor) + } + elseif ($prop.Name -eq 'Message') { + $value = $errorColor + $value + } + + $isFirstLine = $true + if ($value.Contains($newline)) { + # the 3 is to account for ' : ' + $valueIndent = ' ' * ($propLength + 3) + # need to trim any extra whitespace already in the text + foreach ($line in $value.Split($newline)) { + if (!$isFirstLine) { + $null = $output.Append("${newline}${prefix}${valueIndent}") + } + $null = $output.Append($line.Trim()) + $isFirstLine = $false + } + } + else { + $null = $output.Append($value) + } + } + } + + $null = $output.Append($newline) + } + } + + # if we had added nested properties, we need to remove the last newline + if ($addedProperty) { + $null = $output.Remove($output.Length - $newline.Length, $newline.Length) + } + + $output.ToString() +} + +# Add back original typename and remove PSExtendedError +if ($_.PSObject.TypeNames.Contains('System.Management.Automation.ErrorRecord#PSExtendedError')) { + $_.PSObject.TypeNames.Add('System.Management.Automation.ErrorRecord') + $null = $_.PSObject.TypeNames.Remove('System.Management.Automation.ErrorRecord#PSExtendedError') +} +elseif ($_.PSObject.TypeNames.Contains('System.Exception#PSExtendedError')) { + $_.PSObject.TypeNames.Add('System.Exception') + $null = $_.PSObject.TypeNames.Remove('System.Exception#PSExtendedError') +} + +Show-ErrorRecord $_ +#> \ No newline at end of file diff --git a/Reference/detail.ps1 b/Reference/detail.ps1 new file mode 100644 index 0000000..79e9c36 --- /dev/null +++ b/Reference/detail.ps1 @@ -0,0 +1,183 @@ +<# +[int]$maxDepth = 10 +Set-StrictMode -Off + +$ellipsis = "`u{2026}" +$resetColor = '' +$errorColor = '' +$accentColor = '' + +if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = $PSStyle.Reset + $errorColor = $psstyle.Formatting.Error + $accentColor = $PSStyle.Formatting.FormatAccent +} + +function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) { + $newline = [Environment]::Newline + $output = [System.Text.StringBuilder]::new() + $prefix = ' ' * $indent + + $expandTypes = @( + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) + + # if object is an Exception, add an ExceptionType property + if ($obj -is [Exception]) { + $obj | Add-Member -NotePropertyName Type -NotePropertyValue $obj.GetType().FullName -ErrorAction Ignore + } + + # first find the longest property so we can indent properly + $propLength = 0 + foreach ($prop in $obj.PSObject.Properties) { + if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { + $propLength = $prop.Name.Length + } + } + + $addedProperty = $false + foreach ($prop in $obj.PSObject.Properties) { + + # don't show empty properties or our added property for $error[index] + if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { + $addedProperty = $true + $null = $output.Append($prefix) + $null = $output.Append($accentColor) + $null = $output.Append($prop.Name) + $propNameIndent = ' ' * ($propLength - $prop.Name.Length) + $null = $output.Append($propNameIndent) + $null = $output.Append(' : ') + $null = $output.Append($resetColor) + + $newIndent = $indent + 4 + + # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes + if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or + $expandTypes -contains $prop.TypeNameOfValue -or ($prop.TypeNames -ne $null -and $expandTypes -contains $prop.TypeNames[0])) { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $null = $output.Append($newline) + $null = $output.Append((Show-ErrorRecord $prop.Value $newIndent ($depth + 1))) + } + } + # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members + elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $targetSite = [PSCustomObject]@{ + Name = $prop.Value.Name + DeclaringType = $prop.Value.DeclaringType + MemberType = $prop.Value.MemberType + Module = $prop.Value.Module + } + + $null = $output.Append($newline) + $null = $output.Append((Show-ErrorRecord $targetSite $newIndent ($depth + 1))) + } + } + # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation + elseif ($prop.Name -eq 'StackTrace') { + # for a stacktrace which is usually quite wide with info, we left justify it + $null = $output.Append($newline) + $null = $output.Append($prop.Value) + } + # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here + elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { + $isFirstElement = $true + foreach ($key in $prop.Value.Keys) { + if ($isFirstElement) { + $null = $output.Append($newline) + } + + if ($key -eq 'Authorization') { + $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}${ellipsis}${newline}") + } + else { + $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}$($prop.Value[$key])${newline}") + } + + $isFirstElement = $false + } + } + # if the object implements IEnumerable and not a string, we try to show each object + # We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here + elseif (!($prop.Value -is [System.String]) -and $prop.Value.GetType().GetInterface('IEnumerable') -ne $null -and $prop.Name -ne 'Data') { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $isFirstElement = $true + foreach ($value in $prop.Value) { + $null = $output.Append($newline) + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1))) + $isFirstElement = $false + } + } + } + # Anything else, we convert to string. + # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error + else { + $value = $null + if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $value -ne $null) + { + if ($prop.Name -eq 'PositionMessage') { + $value = $value.Insert($value.IndexOf('~'), $errorColor) + } + elseif ($prop.Name -eq 'Message') { + $value = $errorColor + $value + } + + $isFirstLine = $true + if ($value.Contains($newline)) { + # the 3 is to account for ' : ' + $valueIndent = ' ' * ($propLength + 3) + # need to trim any extra whitespace already in the text + foreach ($line in $value.Split($newline)) { + if (!$isFirstLine) { + $null = $output.Append("${newline}${prefix}${valueIndent}") + } + $null = $output.Append($line.Trim()) + $isFirstLine = $false + } + } + else { + $null = $output.Append($value) + } + } + } + + $null = $output.Append($newline) + } + } + + # if we had added nested properties, we need to remove the last newline + if ($addedProperty) { + $null = $output.Remove($output.Length - $newline.Length, $newline.Length) + } + + $output.ToString() +} + +# Add back original typename and remove PSExtendedError +if ($_.PSObject.TypeNames.Contains('System.Management.Automation.ErrorRecord#PSExtendedError')) { + $_.PSObject.TypeNames.Add('System.Management.Automation.ErrorRecord') + $null = $_.PSObject.TypeNames.Remove('System.Management.Automation.ErrorRecord#PSExtendedError') +} +elseif ($_.PSObject.TypeNames.Contains('System.Exception#PSExtendedError')) { + $_.PSObject.TypeNames.Add('System.Exception') + $null = $_.PSObject.TypeNames.Remove('System.Exception#PSExtendedError') +} + +Show-ErrorRecord $_ +#> \ No newline at end of file diff --git a/source/ErrorView.format.ps1xml b/source/ErrorView.format.ps1xml index 443e23a..028b7b5 100644 --- a/source/ErrorView.format.ps1xml +++ b/source/ErrorView.format.ps1xml @@ -68,5 +68,35 @@ </CustomEntries> </CustomControl> </View> + <View> + <Name>InformationRecord</Name> + <OutOfBand /> + <ViewSelectedBy> + <TypeName>System.Management.Automation.InformationRecord</TypeName> + </ViewSelectedBy> + <CustomControl> + <CustomEntries> + <CustomEntry> + <CustomItem> + <ExpressionBinding> + <ScriptBlock> + $_.MessageData | Format-List * | Out-String + </ScriptBlock> + </ExpressionBinding> + <ExpressionBinding> + <ScriptBlock> + "Tags: " + @($_.Tags -join ", ") + </ScriptBlock> + </ExpressionBinding> + <ExpressionBinding> + <ScriptBlock> + $_ | Select-Object * -ExcludeProperty Tags, MessageData | Format-List * -Force | Out-String + </ScriptBlock> + </ExpressionBinding> + </CustomItem> + </CustomEntry> + </CustomEntries> + </CustomControl> + </View> </ViewDefinitions> </Configuration> diff --git a/source/prefix.ps1 b/source/prefix.ps1 index 9f73337..aef18c4 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1,8 +1,53 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'ErrorView is all about the ErrorView global variable')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Seriously. Stop complaining about ErrorView')] param( $global:ErrorView = "Simple" ) # We need to _overwrite_ the ErrorView # So -PrependPath, instead of FormatsToProcess -Update-FormatData -PrependPath $PSScriptRoot\ErrorView.format.ps1xml \ No newline at end of file +Update-FormatData -PrependPath $PSScriptRoot\ErrorView.format.ps1xml + +Set-StrictMode -Off + +# Borrowed this one from https://github.com/chalk/ansi-regex +$script:AnsiEscapes = [Regex]::new("([\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?(?:\u001b\u005c|\u0007))|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])))", "Compiled"); + +# starting with an escape character and then... +# ESC ] <anything> <ST> - where ST is either 1B 5C or 7 (BEL, aka `a) +# ESC [ non-letters letter (or ~, =, @, >) +# ESC ( <any character> +# ESC O P +# ESC O Q +# ESC O R +# ESC O S +# $script:AnsiEscapes = [Regex]::new("\x1b[\(\)%`"&\.\/*+.-][@-Z]|\x1b\].*?(?:\u001b\u005c|\u0007|^)|\x1b\[\P{L}*[@-_A-Za-z^`\{\|\}~]|\x1b#\d|\x1b[!-~]", [System.Text.RegularExpressions.RegexOptions]::Compiled); + + + + + +$script:ellipsis = [char]0x2026 +$script:newline = [Environment]::Newline +$script:resetColor = '' +$script:errorColor = '' +$script:accentColor = '' +$script:errorAccentColor = '' +$script:LineColors = @( + "`e[38;2;255;255;255m" + "`e[38;2;179;179;179m" +) + +if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + if ($PSStyle) { + $script:resetColor = $PSStyle.Reset + $script:errorColor = $PSStyle.Formatting.Error + $script:accentColor = $PSStyle.Formatting.FormatAccent + $script:errorAccentColor = $PSStyle.Formatting.ErrorAccent + } else { + $script:resetColor = "$([char]27)[0m" + $script:errorColor = "$([char]27)[31m" + $script:accentColor = "$([char]27)[32;1m" + $script:errorAccentColor = "$([char]27)[31;1m" + } +} \ No newline at end of file diff --git a/source/private/GetListRecursive.ps1 b/source/private/GetListRecursive.ps1 new file mode 100644 index 0000000..a6cdd05 --- /dev/null +++ b/source/private/GetListRecursive.ps1 @@ -0,0 +1,160 @@ +function GetListRecursive { + <# + .SYNOPSIS + Internal implementation of the Detailed error view to support recursion and indentation + #> + [CmdletBinding()] + param( + $InputObject, + [int]$indent = 0, + [int]$depth = 1 + ) + Write-Information "ENTER GetListRecursive END $($InputObject.GetType().FullName) $indent $depth" -Tags 'Trace', 'Enter', 'GetListRecursive' + Write-Information (Get-PSCallStack) -Tags 'Trace', 'StackTrace', 'GetListRecursive' + $output = [System.Text.StringBuilder]::new() + $prefix = ' ' * $indent + + $expandTypes = @( + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) + + # The built-in DetailedView aligns all the ":" characters, so we need to find the longest property name + $propLength = 0 + foreach ($prop in $InputObject.PSObject.Properties) { + if ($null -ne $prop.Value -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { + $propLength = $prop.Name.Length + } + } + + $addedProperty = $false + foreach ($prop in $InputObject.PSObject.Properties) { + # don't show empty properties or our added property for $error[index] + if ($null -ne $prop.Value -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { + $addedProperty = $true + $null = $output.Append($prefix) + $null = $output.Append($accentColor) + $null = $output.Append($prop.Name) + $null = $output.Append(' ',($propLength - $prop.Name.Length)) + $null = $output.Append(' : ') + $null = $output.Append($resetColor) + + $newIndent = $indent + 2 + + # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes + if ($prop.Value -is [Exception] -or + $prop.Value -is [System.Management.Automation.ErrorRecord] -or + $expandTypes -contains $prop.TypeNameOfValue -or + ($null -ne $prop.TypeNames -and $expandTypes -contains $prop.TypeNames[0])) { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } else { + if ($prop.Value -is [Exception]) { + $null = $output.Append($newline) + $null = $output.Append(( + GetListRecursive ([PSCustomObject]@{ + "Type" = $errorAccentColor + $prop.Value.GetType().FullName + $resetColor + }) $newIndent ($depth + 1) + )) + } + $null = $output.Append($newline) + $null = $output.Append((GetListRecursive $prop.Value $newIndent ($depth + 1))) + } + } elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { + # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } else { + $targetSite = [PSCustomObject]@{ + Name = $prop.Value.Name + DeclaringType = $prop.Value.DeclaringType + MemberType = $prop.Value.MemberType + Module = $prop.Value.Module + } + + $null = $output.Append($newline) + $null = $output.Append((GetListRecursive $targetSite $newIndent ($depth + 1))) + } + } elseif ($prop.Name -eq 'StackTrace') { + # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation + # for a stacktrace which is usually quite wide with info, we left justify it + $null = $output.Append($newline) + $null = $output.Append($prop.Value) + } elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { + # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here + $isFirstElement = $true + foreach ($key in $prop.Value.Keys) { + if ($isFirstElement) { + $null = $output.Append($newline) + } + + if ($key -eq 'Authorization') { + $null = $output.Append("${prefix} ${accentColor}${key}: ${resetColor}${ellipsis}${newline}") + } else { + $null = $output.Append("${prefix} ${accentColor}${key}: ${resetColor}$($prop.Value[$key])${newline}") + } + + $isFirstElement = $false + } + } elseif (!($prop.Value -is [System.String]) -and $null -ne $prop.Value.GetType().GetInterface('IEnumerable') -and $prop.Name -ne 'Data') { + # if the object implements IEnumerable and not a string, we try to show each object + # We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } else { + $isFirstElement = $true + foreach ($value in $prop.Value) { + $null = $output.Append($newline) + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((GetListRecursive $value $newIndent ($depth + 1))) + $isFirstElement = $false + } + } + } else { + # Anything else, we convert to string. + # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error + $value = $null + if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $null -ne $value) { + $value = $value.Trim() + if ($prop.Name -eq 'PositionMessage') { + $value = $value.Insert($value.IndexOf('~'), $errorColor) + } elseif ($prop.Name -eq 'Message') { + $value = $errorColor + $value + } + + $isFirstLine = $true + if ($value.Contains($newline)) { + # the 3 is to account for ' : ' + # $valueIndent = ' ' * ($prop.Name.Length + 2) + $valueIndent = ' ' * ($propLength + 3) + # need to trim any extra whitespace already in the text + foreach ($line in $value.Split($newline)) { + if (!$isFirstLine) { + $null = $output.Append("${newline}${prefix}${valueIndent}") + } + $null = $output.Append($line.Trim()) + $isFirstLine = $false + } + } else { + $null = $output.Append($value) + } + } + } + + $null = $output.Append($newline) + } + } + + # if we had added nested properties, we need to remove the last newline + if ($addedProperty) { + $null = $output.Remove($output.Length - $newline.Length, $newline.Length) + } + + $output.ToString() + Write-Information "EXIT GetListRecursive END $($InputObject.GetType().FullName) $indent $depth (of $maxDepth)" -Tags 'Trace', 'Enter', 'GetListRecursive' +} diff --git a/source/private/GetYamlRecursive.ps1 b/source/private/GetYamlRecursive.ps1 new file mode 100644 index 0000000..ac3880e --- /dev/null +++ b/source/private/GetYamlRecursive.ps1 @@ -0,0 +1,169 @@ +function GetYamlRecursive { + <# + .SYNOPSIS + Creates a description of an ErrorRecord that looks like valid Yaml + .DESCRIPTION + This produces valid Yaml output from ErrorRecord you pass to it, recursively. + #> + [CmdletBinding()] + param( + # The object that you want to convert to YAML + [Parameter(Mandatory, ValueFromPipeline)] + $InputObject, + + # Optionally, a limit on the depth to recurse properties (defaults to 16) + [parameter()] + [int]$depth = 16, + + # If set, include empty and null properties in the output + [switch]$IncludeEmpty, + + # Recursive use only. Handles indentation for formatting + [parameter(DontShow)] + [int]$NestingLevel = 0, + + # use OuterXml instead of treating XmlDocuments like objects + [parameter(DontShow)] + [switch]$XmlAsXml + ) + process { + $wrap = [Console]::BufferWidth - 1 - ($NestingLevel * 2) + @( + if ($Null -eq $InputObject) { return 'null' } # if it is null return null + if ($NestingLevel -eq 0 -and $local:__hasoutput) { '---' } # if we have output before, add a yaml separator + $__hasoutput = $true + $padding = "`n$(' ' * $NestingLevel)" # lets just create our left-padding for the block + $Recurse = @{ + 'Depth' = $depth - 1 + 'NestingLevel' = $NestingLevel + 1 + 'XmlAsXml' = $XmlAsXml + } + $Wrap = + + try { + switch ($InputObject) { + # prevent these values being expanded + <# if ($Type -in @( 'guid', + , 'datatable', 'List`1','SqlDataReader', 'datarow', 'type', + 'MemberTypes', 'RuntimeModule', 'RuntimeType', 'ErrorCategoryInfo', 'CommandInfo', 'CmdletInfo' )) { + #> + { $InputObject -is [scriptblock] } { + "{$($InputObject.ToString())}" + break + } + { $InputObject -is [type] } { + "'[$($InputObject.FullName)]'" + break + } + { $InputObject -is [System.Xml.XmlDocument] -or $InputObject -is [System.Xml.XmlElement] } { + "|" + $InputObject.OuterXml | WrapString $Wrap $padding -Colors:$LineColors + break + } + { $InputObject -is [datetime] -or $InputObject -is [datetimeoffset] } { + # s=SortableDateTimePattern (based on ISO 8601) using local time + $InputObject.ToString('s') + break + } + { $InputObject -is [timespan] -or $InputObject -is [version] -or $InputObject -is [uri] } { + # s=SortableDateTimePattern (based on ISO 8601) using local time + "'$InputObject'" + break + } + # yaml case for booleans + { $InputObject -is [bool] } { + if ($InputObject) { 'true' } else { 'false' } + break + } + # If we're going to go over our depth, just output like it's a value type + # ValueTypes are just output with no possibility of wrapping or recursion + { $InputObject -is [Enum] -or $InputObject.GetType().BaseType -eq [ValueType] -or $depth -eq 1 } { + "$InputObject" + break + } + # 'PSNoteProperty' { + # # Write-Verbose "$($padding)Show $($property.Name)" + # GetYamlRecursive -InputObject $InputObject.Value @Recurse } + { $InputObject -is [System.Collections.IDictionary] } { + foreach ($kvp in $InputObject.GetEnumerator()) { + # Write-Verbose "$($padding)Enumerate $($property.Name)" + "$padding$accentColor$($kvp.Name):$resetColor " + + (GetYamlRecursive -InputObject $kvp.Value @Recurse) + } + break + } + + { $InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string] } { + foreach ($item in $InputObject) { + # Write-Verbose "$($padding)Enumerate $($property.Name)" + $Value = GetYamlRecursive -InputObject $item @Recurse + # if ($Value -ne 'null' -or $IncludeEmpty) { + "$accentColor$padding$resetColor- $Value" + # } + } + break + } + + # Limit recursive enumeration to specific types: + { $InputObject -is [Exception] -or $InputObject -is [System.Management.Automation.ErrorRecord] -or + $InputObject.PSTypeNames[0] -in @( + 'System.Exception' + 'System.Management.Automation.ErrorRecord' + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) } { + # For exceptions, output a fake property for the exception type + if ($InputObject -is [Exception]) { + "$padding${accentColor}#Type:$resetColor ${errorAccentColor}" + $InputObject.GetType().FullName + $resetColor + } + foreach ($property in $InputObject.PSObject.Properties) { + if ($property.Value) { + $Value = GetYamlRecursive -InputObject $property.Value @Recurse + # For special cases, add some color: + if ($property.Name -eq "PositionMessage") { + $Value = $Value -replace "(\+\s+)(~+)", "`$1$errorColor`$2$resetColor" + } + if ($InputObject -is [Exception] -and $property.Name -eq "Message") { + $Value = "$errorColor$Value$resetColor" + } + if ((-not [string]::IsNullOrEmpty($Value) -and $Value -ne 'null' -and $Value.Count -gt 0) -or $IncludeEmpty) { + "$padding$accentColor$($property.Name):$resetColor " + $Value + } + } + } + break + } + # 'generic' { + # foreach($key in $InputObject.Keys) { + # # Write-Verbose "$($padding)Enumerate $($key)" + # $Value = GetYamlRecursive -InputObject $InputObject.$key @Recurse + # if ((-not [string]::IsNullOrEmpty($Value) -and $Value -ne 'null') -or $IncludeEmpty) { + # "$padding$accentColor$($key):$resetColor " + $Value + # } + # } + # } + default { + # Treat anything else as a string + $StringValue = $null + if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($InputObject, [string], [ref]$StringValue) -and $null -ne $StringValue) { + $StringValue = $StringValue.Trim() + if ($StringValue -match '[\r\n]' -or $StringValue.Length -gt $wrap) { + ">" # signal that we are going to use the readable 'newlines-folded' format + $StringValue | WrapString $Wrap $padding -Colors:$LineColors + } elseif ($StringValue.Contains(":")) { + "'$($StringValue -replace '''', '''''')'" # single quote it + } else { + "$($StringValue -replace '''', '''''')" + } + } else { + Write-Warning "Unable to convert $($InputObject.GetType().FullName) to string" + } + } + } + } catch { + Write-Error "Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($InputObject)' Class: $($InputObject.GetType().Name) BaseClass: $($InputObject.GetType().BaseType.Name) " + } + ) -join "" + } +} \ No newline at end of file diff --git a/source/private/WrapString.ps1 b/source/private/WrapString.ps1 new file mode 100644 index 0000000..4d3fbfa --- /dev/null +++ b/source/private/WrapString.ps1 @@ -0,0 +1,119 @@ +$script:AnsiPattern = "[\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?(?:\u001b\u005c|\u0007))|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))" +$script:AnsiRegex = [Regex]::new($AnsiPattern, "Compiled"); +function MeasureString { + [CmdletBinding()] + param( + [string]$InputObject + ) + $AnsiRegex.Replace($InputObject, '').Length +} + + +filter WrapString { + [CmdletBinding()] + param( + # The input string will be wrapped to a certain length, with optional padding on the front + [Parameter(ValueFromPipeline)] + [string]$InputObject, + + # The maximum length of a line. Defaults to [Console]::BufferWidth - 1 + [Parameter(Position=0)] + [int]$Width = ($Host.UI.RawUI.BufferSize.Width), + + # The padding to add to the front of each line to cause indenting. Defaults to empty string. + [Parameter(Position=1)] + [string]$IndentPadding = ([string]::Empty), + + # If set, colors to use for alternating lines + [string[]]$Colors = @(''), + + # If set, will output empty lines for each original new line + [switch]$EmphasizeOriginalNewlines + ) + begin { + $color = 0; + Write-Debug "Colors: $($Colors -replace "`e(.+)", "`e`$1``e`$1")" + # $wrappableChars = [char[]]" ,.?!:;-`n`r`t" + # $maxLength = $width - $IndentPadding.Length -1 + $wrapper = [Regex]::new("((?:$AnsiPattern)*[^-=,.?!:;\s\r\n\t\\\/\|]+(?:$AnsiPattern)*)", "Compiled") + $output = [System.Text.StringBuilder]::new($Colors[$color] + $IndentPadding) + } + process { + foreach($line in $InputObject -split "(\r?\n)") { + # Don't bother trying to split empty lines + if ([String]::IsNullOrWhiteSpace($AnsiRegex.Replace($line, ''))) { + Write-Debug "Empty String ($($line.Length))" + if ($EmphasizeOriginalNewlines) { [string]::Empty } + continue + } + + $slices = $line -split $wrapper | ForEach-Object { @{ Text = $_; Length = MeasureString $_ } } + Write-Debug "$($line.Length) words in line. $($AnsiRegex.Replace($line, ''))" + $lineLength = $IndentPadding.Length + foreach($slice in $slices) { + $lineLength += $slice.Length + if ($lineLength -le $Width) { + Write-Verbose "+ $($slice.Length) = $lineLength < $Width" + $null = $output.Append($slice.Text) + } else { + Write-Verbose "Output $($lineLength - $slice.Length)" + Write-Verbose "+ $($slice.Length) = $($slice.Length)" + $color = ($color + 1) % $Colors.Length + $output.ToString().Trim() + $null = $output.Clear().Append($Colors[$color]).Append($IndentPadding).Append($slice.Text) + $lineLength = $IndentPadding.Length + $slice.Length + } + } + $output.ToString().Trim() + $null = $output.Clear().Append($Colors[$color]).Append($IndentPadding) + } + + # $currentIndex = 0; + # $lastWrap = 0; + # do { + # $lastWrap = $currentIndex; + # #Write-Verbose "m: $Width, l: $($line.Length), c: $color $($Colors[$color] -replace "`e","``e")" + + + + # if ($AnsiEscapes.Replace($first, '').Length -gt $maxLength + 1) { + # # If we couldn't find a good place to wrap, just wrap at the end of the line + # $first = $line.Substring(0, $maxLength+1) + # $line = $line.Substring($maxLength+1) + # } + # $Colors[$color] + $IndentPadding + $first.TrimEnd() + +<# $currentIndex = $( + if ($lastWrap + $Width -gt $line.Length) { + $line.Length + } else { + $line.LastIndexOfAny($wrappableChars, ([Math]::Min(($line.Length - 1), ($lastWrap + $Width)))) + 1 + } + ) + + $slice = $line.Substring($lastWrap, ($currentIndex - $lastWrap)) + if (($slice.Length - $script:AnsiEscapes.Replace($slice,'').Length) -gt 0) { + $currentIndex = $( + if ($lastWrap + $Width -gt $line.Length) { + $line.Length + } else { + $line.LastIndexOfAny($wrappableChars, ([Math]::Min(($line.Length - 1), ($lastWrap + $Width)))) + 1 + } + ) + } + + # If we couldn't find a good place to wrap, just wrap at the end of the line + if ($currentIndex -le $lastWrap) { + $currentIndex = [Math]::Min(($lastWrap + $Width), $line.Length ) + } + # Output the line, with the appropriate color and padding + $Colors[$color] + $IndentPadding + $line.Substring($lastWrap, ($currentIndex - $lastWrap)).TrimEnd() +#> +<# } while($line); + + if ($line -ne "`n") { + $color = ($color + 1) % $Colors.Length + } + } #> + } +} \ No newline at end of file diff --git a/source/public/ConvertTo-ConciseErrorView.ps1 b/source/public/ConvertTo-ConciseErrorView.ps1 new file mode 100644 index 0000000..391014f --- /dev/null +++ b/source/public/ConvertTo-ConciseErrorView.ps1 @@ -0,0 +1,62 @@ +function ConvertTo-ConciseErrorView { + [CmdletBinding()] + param( + [System.Management.Automation.ErrorRecord] + $InputObject + ) + if ("$accentColor".Length) { + $local:accentColor = $script:errorAccentColor + $local:resetColor = $script:resetColor + } else { + $local:accentColor = ">>>" + $local:resetColor = "<<<" + } + + + if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { + $errorColor + $InputObject.Exception.Message + $resetColor + } else { + $myinv = $InputObject.InvocationInfo + if ($myinv -and ($myinv.MyCommand -or ($InputObject.CategoryInfo.Category -ne 'ParserError'))) { + # rip off lines that say "At line:1 char:1" (hopefully, in a language agnostic way) + $posmsg = $myinv.PositionMessage -replace "^At line:1 char:1[\r\n]+" + + # rip off the underline and use the accentcolor instead + $pattern = $posmsg -split "[\r\n]+" -match "\+( +~+)\s*" -replace '(~+)', '($1)' -replace '( +)','($1)' -replace '~| ','.' + $posmsg = $posmsg -replace '[\r\n]+\+ +~+' + if ($pattern) { + $posmsg = $posmsg -replace "\+$pattern", "+`$1$accentColor`$2$resetColor" + } + } else { + $posmsg = "" + } + + if ($posmsg -ne "") { + $posmsg = "`n" + $posmsg + } + + if ( & { Set-StrictMode -Version 1; $InputObject.PSMessageDetails } ) { + $posmsg = " : " + $InputObject.PSMessageDetails + $posmsg + } + + $indent = 4 + $width = $host.UI.RawUI.BufferSize.Width - $indent - 2 + + $originInfo = & { Set-StrictMode -Version 1; $InputObject.OriginInfo } + if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) { + $indentString = "+ PSComputerName : " + $originInfo.PSComputerName + $posmsg += "`n" + foreach ($line in @($indentString -split "(.{$width})")) { + if ($line) { + $posmsg += (" " * $indent + $line) + } + } + } + + if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { + $errorColor + $InputObject.Exception.Message + $resetColor + $posmsg + "`n " + } else { + $errorColor + $InputObject.ErrorDetails.Message + $resetColor + $posmsg + } + } +} \ No newline at end of file diff --git a/source/public/ConvertTo-DetailedErrorView.ps1 b/source/public/ConvertTo-DetailedErrorView.ps1 index fbc1c58..210520c 100644 --- a/source/public/ConvertTo-DetailedErrorView.ps1 +++ b/source/public/ConvertTo-DetailedErrorView.ps1 @@ -21,186 +21,13 @@ function ConvertTo-DetailedErrorView { ) begin { + Write-Information "ENTER ConvertTo-DetailedErrorView BEGIN " -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' - Set-StrictMode -Off - - $ellipsis = "`u{2026}" - $resetColor = '' - $errorColor = '' - $accentColor = '' - $newline = [Environment]::Newline - $OutputRoot = [System.Text.StringBuilder]::new() - - if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - $resetColor = "$([char]0x1b)[0m" - $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } - $accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } - } - - function DetailedErrorView { - <# - .SYNOPSIS - Internal implementation of the Detailed error view to support recursion and indentation - #> - [CmdletBinding()] - param( - $InputObject, - [int]$indent = 0, - [int]$depth = 1 - ) - $prefix = ' ' * $indent - - $expandTypes = @( - 'Microsoft.Rest.HttpRequestMessageWrapper' - 'Microsoft.Rest.HttpResponseMessageWrapper' - 'System.Management.Automation.InvocationInfo' - ) - - # if object is an Exception, add an ExceptionType property - if ($InputObject -is [Exception]) { - $InputObject | Add-Member -NotePropertyName Type -NotePropertyValue $InputObject.GetType().FullName -ErrorAction Ignore - } - - # first find the longest property so we can indent properly - $propLength = 0 - foreach ($prop in $InputObject.PSObject.Properties) { - if ($null -ne $prop.Value -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { - $propLength = $prop.Name.Length - } - } - - $addedProperty = $false - foreach ($prop in $InputObject.PSObject.Properties) { - - # don't show empty properties or our added property for $error[index] - if ($null -ne $prop.Value -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { - $addedProperty = $true - $null = $OutputRoot.Append($prefix) - $null = $OutputRoot.Append($accentColor) - $null = $OutputRoot.Append($prop.Name) - $null = $OutputRoot.Append(' ',($propLength - $prop.Name.Length)) - $null = $OutputRoot.Append(' : ') - $null = $OutputRoot.Append($resetColor) - - $newIndent = $indent + 4 - - # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes - if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or - $expandTypes -contains $prop.TypeNameOfValue -or ($null -ne $prop.TypeNames -and $expandTypes -contains $prop.TypeNames[0])) { - - if ($depth -ge $maxDepth) { - $null = $OutputRoot.Append($ellipsis) - } - else { - $null = $OutputRoot.Append($newline) - $null = $OutputRoot.Append((DetailedErrorView $prop.Value $newIndent ($depth + 1))) - } - } - # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members - elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { - if ($depth -ge $maxDepth) { - $null = $OutputRoot.Append($ellipsis) - } - else { - $targetSite = [PSCustomObject]@{ - Name = $prop.Value.Name - DeclaringType = $prop.Value.DeclaringType - MemberType = $prop.Value.MemberType - Module = $prop.Value.Module - } - - $null = $OutputRoot.Append($newline) - $null = $OutputRoot.Append((DetailedErrorView $targetSite $newIndent ($depth + 1))) - } - } - # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation - elseif ($prop.Name -eq 'StackTrace') { - # for a stacktrace which is usually quite wide with info, we left justify it - $null = $OutputRoot.Append($newline) - $null = $OutputRoot.Append($prop.Value) - } - # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here - elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { - $isFirstElement = $true - foreach ($key in $prop.Value.Keys) { - if ($isFirstElement) { - $null = $OutputRoot.Append($newline) - } - - if ($key -eq 'Authorization') { - $null = $OutputRoot.Append("${prefix} ${accentColor}${key} : ${resetColor}${ellipsis}${newline}") - } - else { - $null = $OutputRoot.Append("${prefix} ${accentColor}${key} : ${resetColor}$($prop.Value[$key])${newline}") - } - - $isFirstElement = $false - } - } - # if the object implements IEnumerable and not a string, we try to show each object - # We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here - elseif (!($prop.Value -is [System.String]) -and $null -ne $prop.Value.GetType().GetInterface('IEnumerable') -and $prop.Name -ne 'Data') { - - if ($depth -ge $maxDepth) { - $null = $OutputRoot.Append($ellipsis) - } - else { - $isFirstElement = $true - foreach ($value in $prop.Value) { - $null = $OutputRoot.Append($newline) - if (!$isFirstElement) { - $null = $OutputRoot.Append($newline) - } - $null = $OutputRoot.Append((DetailedErrorView $value $newIndent ($depth + 1))) - $isFirstElement = $false - } - } - } - # Anything else, we convert to string. - # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error - else { - $value = $null - if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $null -ne $value) - { - if ($prop.Name -eq 'PositionMessage') { - $value = $value.Insert($value.IndexOf('~'), $errorColor) - } - elseif ($prop.Name -eq 'Message') { - $value = $errorColor + $value - } - - $isFirstLine = $true - if ($value.Contains($newline)) { - # the 3 is to account for ' : ' - $valueIndent = ' ' * ($propLength + 3) - # need to trim any extra whitespace already in the text - foreach ($line in $value.Split($newline)) { - if (!$isFirstLine) { - $null = $OutputRoot.Append("${newline}${prefix}${valueIndent}") - } - $null = $OutputRoot.Append($line.Trim()) - $isFirstLine = $false - } - } - else { - $null = $OutputRoot.Append($value) - } - } - } - - $null = $OutputRoot.Append($newline) - } - } - - # if we had added nested properties, we need to remove the last newline - if ($addedProperty) { - $null = $OutputRoot.Remove($OutputRoot.Length - $newline.Length, $newline.Length) - } - - $OutputRoot.ToString() - } + Write-Information "EXIT ConvertTo-DetailedErrorView BEGIN" -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' } process { - DetailedErrorView $InputObject + Write-Information "ENTER ConvertTo-DetailedErrorView PROCESS $($InputObject.GetType().FullName)" -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' + GetListRecursive $InputObject + Write-Information "EXIT ConvertTo-DetailedErrorView PROCESS $($InputObject.GetType().FullName)" -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' } } diff --git a/source/public/ConvertTo-FullErrorView.ps1 b/source/public/ConvertTo-FullErrorView.ps1 index 5377ef8..c13705e 100644 --- a/source/public/ConvertTo-FullErrorView.ps1 +++ b/source/public/ConvertTo-FullErrorView.ps1 @@ -22,13 +22,10 @@ filter ConvertTo-FullErrorView { $resetColor = "$([char]0x1b)[0m" $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } - #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } - $Detail = $InputObject | Format-List * -Force | Out-String -Width 120 - $Detail = $Detail -replace "((?:Exception|FullyQualifiedErrorId).*`e\[0m)(.*)", "$($PSStyle.Formatting.ErrorAccent)`$1$($PSStyle.Formatting.Error)`$2$($PSStyle.Reset)" - } else { - $Detail = $InputObject | Format-List * -Force | Out-String -Width 120 } + $Detail = $InputObject | Format-List * -Force | Out-String -Width 120 + # NOTE: ErrorViewRecurse is normally false, and only set temporarily by Format-Error -Recurse if ($ErrorViewRecurse) { $Count = 1 diff --git a/source/public/ConvertTo-NormalErrorView.ps1 b/source/public/ConvertTo-NormalErrorView.ps1 index cdcd820..a33dc72 100644 --- a/source/public/ConvertTo-NormalErrorView.ps1 +++ b/source/public/ConvertTo-NormalErrorView.ps1 @@ -11,17 +11,8 @@ filter ConvertTo-NormalErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) - $resetColor = '' - $errorColor = '' - #$accentColor = '' - if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - $resetColor = "$([char]0x1b)[0m" - $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } - #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } - } - - if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { + if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { $errorColor + $InputObject.Exception.Message + $resetColor } else { $myinv = $InputObject.InvocationInfo @@ -44,9 +35,9 @@ filter ConvertTo-NormalErrorView { $errorCategoryMsg = &{ Set-StrictMode -Version 1; $InputObject.ErrorCategory_Message } if ($null -ne $errorCategoryMsg) { - $indentString = "+ CategoryInfo : " + $InputObject.ErrorCategory_Message + $indentString = $accentColor + "+ CategoryInfo : " + $resetColor + $InputObject.ErrorCategory_Message } else { - $indentString = "+ CategoryInfo : " + $InputObject.CategoryInfo + $indentString = $accentColor + "+ CategoryInfo : " + $resetColor + $InputObject.CategoryInfo } $posmsg += "`n" foreach ($line in @($indentString -split "(.{$width})")) { @@ -55,7 +46,7 @@ filter ConvertTo-NormalErrorView { } } - $indentString = "+ FullyQualifiedErrorId : " + $InputObject.FullyQualifiedErrorId + $indentString = $accentColor + "+ FullyQualifiedErrorId: " + $resetColor + $InputObject.FullyQualifiedErrorId $posmsg += "`n" foreach ($line in @($indentString -split "(.{$width})")) { if ($line) { @@ -65,7 +56,7 @@ filter ConvertTo-NormalErrorView { $originInfo = &{ Set-StrictMode -Version 1; $InputObject.OriginInfo } if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) { - $indentString = "+ PSComputerName : " + $originInfo.PSComputerName + $indentString = "+ PSComputerName : " + $originInfo.PSComputerName $posmsg += "`n" foreach ($line in @($indentString -split "(.{$width})")) { if ($line) { @@ -75,9 +66,9 @@ filter ConvertTo-NormalErrorView { } if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { - $errorColor + $InputObject.Exception.Message + $posmsg + $resetColor + "`n " + $errorColor + $InputObject.Exception.Message + $resetColor + $posmsg + "`n " } else { - $errorColor + $InputObject.ErrorDetails.Message + $posmsg + $resetColor + $errorColor + $InputObject.ErrorDetails.Message + $resetColor + $posmsg } } } \ No newline at end of file diff --git a/source/public/ConvertTo-NormalExceptionView.ps1 b/source/public/ConvertTo-NormalExceptionView.ps1 index fea1db0..929b040 100644 --- a/source/public/ConvertTo-NormalExceptionView.ps1 +++ b/source/public/ConvertTo-NormalExceptionView.ps1 @@ -21,6 +21,6 @@ filter ConvertTo-NormalExceptionView { #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } } - $errorColor + $InputObject.Exception.Message + $resetColor + $errorColor + $InputObject.Message + $resetColor } \ No newline at end of file diff --git a/source/public/ConvertTo-SimpleErrorView.ps1 b/source/public/ConvertTo-SimpleErrorView.ps1 index 97197ff..c8b836b 100644 --- a/source/public/ConvertTo-SimpleErrorView.ps1 +++ b/source/public/ConvertTo-SimpleErrorView.ps1 @@ -14,28 +14,17 @@ function ConvertTo-SimpleErrorView { #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } } - if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { + if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { $InputObject.Exception.Message } else { $myinv = $InputObject.InvocationInfo if ($myinv -and ($myinv.MyCommand -or ($InputObject.CategoryInfo.Category -ne 'ParserError'))) { # rip off lines that say "At line:1 char:1" (hopefully, in a language agnostic way) - $posmsg = $myinv.PositionMessage -replace "^At line:1 .*[\r\n]+" - # rip off the underline and instead, put >>>markers<<< around the important bit - # we could, instead, set the background to a highlight color? - $pattern = $posmsg -split "[\r\n]+" -match "\+( +~+)\s*" -replace '(~+)', '($1)' -replace '( +)','($1)' -replace '~| ','.' - $posmsg = $posmsg -replace '[\r\n]+\+ +~+' - if ($pattern) { - $posmsg = $posmsg -replace "\+$pattern", '+ $1>>>$2<<<' - } + $posmsg = "`n" + $myinv.PositionMessage -replace "^At line:1 .*[\r\n]+" } else { $posmsg = "" } - if ($posmsg -ne "") { - $posmsg = "`n" + $posmsg - } - if ( & { Set-StrictMode -Version 1; $InputObject.PSMessageDetails } ) { $posmsg = " : " + $InputObject.PSMessageDetails + $posmsg } diff --git a/source/public/ConvertTo-YamlErrorView.ps1 b/source/public/ConvertTo-YamlErrorView.ps1 new file mode 100644 index 0000000..704bc2d --- /dev/null +++ b/source/public/ConvertTo-YamlErrorView.ps1 @@ -0,0 +1,33 @@ +function ConvertTo-YamlErrorView { + <# + .SYNOPSIS + Creates a description of an ErrorRecord that looks like valid Yaml + .DESCRIPTION + This produces valid Yaml output from ErrorRecord you pass to it, recursively. + #> + [CmdletBinding()] + param( + # The object that you want to convert to YAML + [Parameter(Mandatory, ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject, + + # Optionally, a limit on the depth to recurse properties (defaults to 16) + [parameter()] + [int]$depth = 16, + + # If set, include empty and null properties in the output + [switch]$IncludeEmpty, + + # Recursive use only. Handles indentation for formatting + [parameter(DontShow)] + [int]$NestingLevel = 0, + + # use OuterXml instead of treating XmlDocuments like objects + [parameter(DontShow)] + [switch]$XmlAsXml + ) + process { + GetYamlRecursive $InputObject + } +} \ No newline at end of file diff --git a/source/public/Format-Error.ps1 b/source/public/Format-Error.ps1 index 2e7edf2..6a28a37 100644 --- a/source/public/Format-Error.ps1 +++ b/source/public/Format-Error.ps1 @@ -1,13 +1,13 @@ function Format-Error { <# .SYNOPSIS - Formats an error for the screen using a specified error view + Formats an error (or exception) for the screen using a specified error view .DESCRIPTION Temporarily switches the error view and outputs the errors .EXAMPLE Format-Error - Shows the Normal error view for the most recent error + Shows the Detailed error view for the most recent error (changed to be compatible with Get-Error) .EXAMPLE $error[0..4] | Format-Error Full @@ -17,8 +17,8 @@ function Format-Error { Shows the full error view of the specific error, recursing into the inner exceptions (if that's supported by the view) #> - [CmdletBinding(DefaultParameterSetName="Count")] - [Alias("fe", "Get-Error")] + [CmdletBinding(DefaultParameterSetName = "InputObject")] + [Alias("fe"<#, "Get-Error"#>)] [OutputType([System.Management.Automation.ErrorRecord])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'The ArgumentCompleter parameters are the required method signature')] @@ -28,7 +28,7 @@ function Format-Error { [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) [System.Management.Automation.CompletionResult[]](( - Get-Command ConvertTo-*ErrorView -ListImported -ParameterName InputObject -ParameterType [System.Management.Automation.ErrorRecord] + Get-Command ConvertTo-*ErrorView -ListImported -ParameterName InputObject -ParameterType [System.Management.Automation.ErrorRecord], [System.Exception] ).Name -replace "ConvertTo-(.*)ErrorView",'$1' -like "*$($wordToComplete)*") })] $View = "Detailed", @@ -47,19 +47,22 @@ function Format-Error { } ), - # Allows ErrorView functions to recurse to InnerException + # Encourages ErrorView functions to recurse InnerException properties [switch]$Recurse ) begin { $ErrorActionPreference = "Continue" - $ErrorView, $View = $View, $ErrorView - [bool]$Recurse, [bool]$ErrorViewRecurse = [bool]$ErrorViewRecurse, $Recurse + + $local:_ErrorView, $global:ErrorView = $global:ErrorView, $View + $local:_ErrorViewRecurse, [bool]$global:ErrorViewRecurse = [bool]$global:ErrorViewRecurse, $Recurse } process { $InputObject } end { - [bool]$ErrorViewRecurse = $Recurse - $ErrorView = $View + $global:ErrorView = $local:_ErrorView + if ($null -ne $local:_ErrorViewRecurse) { + [bool]$global:ErrorViewRecurse = $local:_ErrorViewRecurse + } } } diff --git a/source/public/Set-ErrorView.ps1 b/source/public/Set-ErrorView.ps1 index 8041415..340394a 100644 --- a/source/public/Set-ErrorView.ps1 +++ b/source/public/Set-ErrorView.ps1 @@ -27,6 +27,7 @@ filter Set-ErrorView { ).Name -replace "ConvertTo-(\w+)ErrorView", '$1View' | Select-Object -Unique $ofs = ';' - .([ScriptBlock]::Create("enum ErrorView { $Names }")) + . ([ScriptBlock]::Create("enum ErrorView { $Names }")) + [ErrorView]$global:ErrorView = $View } diff --git a/source/public/Write-NativeCommandError.ps1 b/source/public/Write-NativeCommandError.ps1 index 85d98d1..5ab8889 100644 --- a/source/public/Write-NativeCommandError.ps1 +++ b/source/public/Write-NativeCommandError.ps1 @@ -12,36 +12,40 @@ function Write-NativeCommandError { $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } $accentColor = $PSStyle.Formatting.ErrorAccent } + if ($InputObject -is [System.Exception]) { + $errorColor + $InputObject.GetType().FullName + " : " + $resetColor + } - if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { return } - - $invoc = $InputObject.InvocationInfo - if ($invoc -and $invoc.MyCommand) { - switch -regex ( $invoc.MyCommand.CommandType ) { - ([System.Management.Automation.CommandTypes]::ExternalScript) { - if ($invoc.MyCommand.Path) { - $accentColor + $invoc.MyCommand.Path + " : " + $resetColor + # @('NativeCommandErrorMessage', 'NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView', 'ConciseView', 'DetailedView') -notcontains $ErrorView + if (@('NativeCommandErrorMessage', 'NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView', 'ConciseView', 'DetailedView') -notcontains $ErrorView) { + $invoc = $InputObject.InvocationInfo + if ($invoc -and $invoc.MyCommand) { + switch -regex ( $invoc.MyCommand.CommandType ) { + ([System.Management.Automation.CommandTypes]::ExternalScript) { + if ($invoc.MyCommand.Path) { + $errorColor + $invoc.MyCommand.Path + " : " + $resetColor + } + break } - break - } - ([System.Management.Automation.CommandTypes]::Script) { - if ($invoc.MyCommand.ScriptBlock) { - $accentColor + $invoc.MyCommand.ScriptBlock.ToString() + " : " + $resetColor + ([System.Management.Automation.CommandTypes]::Script) { + if ($invoc.MyCommand.ScriptBlock) { + $errorColor + $invoc.MyCommand.ScriptBlock.ToString() + " : " + $resetColor + } + break } - break - } - default { - if ($invoc.InvocationName -match '^[&\.]?$') { - if ($invoc.MyCommand.Name) { - $accentColor + $invoc.MyCommand.Name + " : " + $resetColor + default { + if ($invoc.InvocationName -match '^[&\.]?$') { + if ($invoc.MyCommand.Name) { + $errorColor + $invoc.MyCommand.Name + " : " + $resetColor + } + } else { + $errorColor + $invoc.InvocationName + " : " + $resetColor } - } else { - $accentColor + $invoc.InvocationName + " : " + $resetColor + break } - break } + } elseif ($invoc -and $invoc.InvocationName) { + $errorColor + $invoc.InvocationName + " : " + $resetColor } - } elseif ($invoc -and $invoc.InvocationName) { - $accentColor + $invoc.InvocationName + " : " + $resetColor } } \ No newline at end of file From 9ace773e700882ab384603a6a5176cc817bc4425 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Fri, 27 Sep 2024 01:56:10 -0400 Subject: [PATCH 03/15] Start by fixing the views. This is NormalView --- Reference/NativeCommandError.ps1 | 56 ++++++ Reference/default.ps1 | 113 ++++------- Reference/detail copy.ps1 | 183 ------------------ Reference/detail.ps1 | 27 ++- Reference/legacy.ps1 | 57 ++++++ source/ErrorView.format.ps1xml | 14 +- source/prefix.ps1 | 26 +-- source/private/Recolor.ps1 | 11 ++ source/private/WrapString.ps1 | 72 ++----- source/public/ConvertTo-NormalErrorView.ps1 | 83 ++++---- .../public/ConvertTo-NormalExceptionView.ps1 | 14 +- source/public/Get-ErrorPrefix.ps1 | 46 +++++ source/public/Write-NativeCommandError.ps1 | 51 ----- 13 files changed, 294 insertions(+), 459 deletions(-) create mode 100644 Reference/NativeCommandError.ps1 delete mode 100644 Reference/detail copy.ps1 create mode 100644 Reference/legacy.ps1 create mode 100644 source/private/Recolor.ps1 create mode 100644 source/public/Get-ErrorPrefix.ps1 delete mode 100644 source/public/Write-NativeCommandError.ps1 diff --git a/Reference/NativeCommandError.ps1 b/Reference/NativeCommandError.ps1 new file mode 100644 index 0000000..48f7007 --- /dev/null +++ b/Reference/NativeCommandError.ps1 @@ -0,0 +1,56 @@ +$errorColor = '' +$commandPrefix = '' +if (@('NativeCommandErrorMessage','NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView','ConciseView','DetailedView') -notcontains $ErrorView) +{ + $myinv = $_.InvocationInfo + if ($Host.UI.SupportsVirtualTerminal) { + $errorColor = $PSStyle.Formatting.Error + } + + $commandPrefix = if ($myinv -and $myinv.MyCommand) { + switch -regex ( $myinv.MyCommand.CommandType ) + { + ([System.Management.Automation.CommandTypes]::ExternalScript) + { + if ($myinv.MyCommand.Path) + { + $myinv.MyCommand.Path + ' : ' + } + + break + } + + ([System.Management.Automation.CommandTypes]::Script) + { + if ($myinv.MyCommand.ScriptBlock) + { + $myinv.MyCommand.ScriptBlock.ToString() + ' : ' + } + + break + } + default + { + if ($myinv.InvocationName -match '^[&\.]?$') + { + if ($myinv.MyCommand.Name) + { + $myinv.MyCommand.Name + ' : ' + } + } + else + { + $myinv.InvocationName + ' : ' + } + + break + } + } + } + elseif ($myinv -and $myinv.InvocationName) + { + $myinv.InvocationName + ' : ' + } +} + +$errorColor + $commandPrefix \ No newline at end of file diff --git a/Reference/default.ps1 b/Reference/default.ps1 index d4f6ac8..6108b80 100644 --- a/Reference/default.ps1 +++ b/Reference/default.ps1 @@ -1,55 +1,3 @@ -<# -if (@('NativeCommandErrorMessage','NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView','ConciseView','DetailedView') -notcontains $ErrorView) -{ - $myinv = $_.InvocationInfo - if ($myinv -and $myinv.MyCommand) - { - switch -regex ( $myinv.MyCommand.CommandType ) - { - ([System.Management.Automation.CommandTypes]::ExternalScript) - { - if ($myinv.MyCommand.Path) - { - $myinv.MyCommand.Path + ' : ' - } - - break - } - - ([System.Management.Automation.CommandTypes]::Script) - { - if ($myinv.MyCommand.ScriptBlock) - { - $myinv.MyCommand.ScriptBlock.ToString() + ' : ' - } - - break - } - default - { - if ($myinv.InvocationName -match '^[&\.]?$') - { - if ($myinv.MyCommand.Name) - { - $myinv.MyCommand.Name + ' : ' - } - } - else - { - $myinv.InvocationName + ' : ' - } - - break - } - } - } - elseif ($myinv -and $myinv.InvocationName) - { - $myinv.InvocationName + ' : ' - } -} - - Set-StrictMode -Off $ErrorActionPreference = 'Stop' trap { 'Error found in error view definition: ' + $_.Exception.Message } @@ -64,6 +12,7 @@ if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__Suppre $errorColor = $PSStyle.Formatting.Error $accentColor = $PSStyle.Formatting.ErrorAccent } + function Get-ConciseViewPositionMessage { # returns a string cut to last whitespace @@ -82,17 +31,30 @@ function Get-ConciseViewPositionMessage { $message = '' $prefix = '' + # Handle case where there is a TargetObject from a Pester `Should` assertion failure and we can show the error at the target rather than the script source + # Note that in some versions, this is a Dictionary<,> and in others it's a hashtable. So we explicitly cast to a shared interface in the method invocation + # to force using `IDictionary.Contains`. Hashtable does have it's own `ContainKeys` as well, but if they ever opt to use a custom `IDictionary`, that may not. + $useTargetObject = $null -ne $err.TargetObject -and + $err.TargetObject -is [System.Collections.IDictionary] -and + ([System.Collections.IDictionary]$err.TargetObject).Contains('Line') -and + ([System.Collections.IDictionary]$err.TargetObject).Contains('LineText') + # The checks here determine if we show line detailed error information: # - check if `ParserError` and comes from PowerShell which eventually results in a ParseException, but during this execution it's an ErrorRecord + $isParseError = $err.CategoryInfo.Category -eq 'ParserError' -and + $err.Exception -is [System.Management.Automation.ParentContainsErrorRecordException] + # - check if invocation is a script or multiple lines in the console + $isMultiLineOrExternal = $myinv.ScriptName -or $myinv.ScriptLineNumber -gt 1 + # - check that it's not a script module as expectation is that users don't want to see the line of error within a module - if ((($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception -is 'System.Management.Automation.ParentContainsErrorRecordException') -or $myinv.ScriptName -or $myinv.ScriptLineNumber -gt 1) -and $myinv.ScriptName -notmatch '\.psm1$') { - $useTargetObject = $false + $shouldShowLineDetail = ($isParseError -or $isMultiLineOrExternal) -and + $myinv.ScriptName -notmatch '\.psm1$' + + if ($useTargetObject -or $shouldShowLineDetail) { - # Handle case where there is a TargetObject and we can show the error at the target rather than the script source - if ($_.TargetObject.Line -and $_.TargetObject.LineText) { - $posmsg = "${resetcolor}$($_.TargetObject.File)${newline}" - $useTargetObject = $true + if ($useTargetObject) { + $posmsg = "${resetcolor}$($err.TargetObject.File)${newline}" } elseif ($myinv.ScriptName) { if ($env:TERM_PROGRAM -eq 'vscode') { @@ -108,8 +70,8 @@ function Get-ConciseViewPositionMessage { } if ($useTargetObject) { - $scriptLineNumber = $_.TargetObject.Line - $scriptLineNumberLength = $_.TargetObject.Line.ToString().Length + $scriptLineNumber = $err.TargetObject.Line + $scriptLineNumberLength = $err.TargetObject.Line.ToString().Length } else { $scriptLineNumber = $myinv.ScriptLineNumber @@ -181,7 +143,7 @@ function Get-ConciseViewPositionMessage { # if rendering line information, break up the message if it's wider than the console if ($myinv -and $myinv.ScriptName -or $err.CategoryInfo.Category -eq 'ParserError') { - $prefixLength = "$([char]27)]8;;{0}`a{1}$([char]27)]8;;`a" -f $pwd, $pwd::new($prefix).ContentLength + $prefixLength = [System.Management.Automation.Internal.StringDecorated]::new($prefix).ContentLength $prefixVtLength = $prefix.Length - $prefixLength # replace newlines in message so it lines up correct @@ -232,8 +194,8 @@ function Get-ConciseViewPositionMessage { $reason = $myinv.MyCommand } # If it's a scriptblock, better to show the command in the scriptblock that had the error - elseif ($_.CategoryInfo.Activity) { - $reason = $_.CategoryInfo.Activity + elseif ($err.CategoryInfo.Activity) { + $reason = $err.CategoryInfo.Activity } elseif ($myinv.MyCommand) { $reason = $myinv.MyCommand @@ -264,9 +226,8 @@ if ($err.FullyQualifiedErrorId -eq 'NativeCommandErrorMessage' -or $err.FullyQua return "${errorColor}$($err.Exception.Message)${resetcolor}" } -$myinv = $err.InvocationInfo if ($ErrorView -eq 'DetailedView') { - $message = Get-Error + $message = Get-Error | Out-String return "${errorColor}${message}${resetcolor}" } @@ -281,10 +242,9 @@ if ($ErrorView -eq 'ConciseView') { } elseif ($myinv -and ($myinv.MyCommand -or ($err.CategoryInfo.Category -ne 'ParserError'))) { $posmsg = $myinv.PositionMessage -} - -if ($posmsg -ne '') { - $posmsg = $newline + $posmsg + if ($posmsg -ne '') { + $posmsg = $newline + $posmsg + } } if ($err.PSMessageDetails) { @@ -292,10 +252,19 @@ if ($err.PSMessageDetails) { } if ($ErrorView -eq 'ConciseView') { + $recommendedAction = $_.ErrorDetails.RecommendedAction + if (-not [String]::IsNullOrWhiteSpace($recommendedAction)) { + $recommendedAction = $newline + + ${errorColor} + + ' Recommendation: ' + + $recommendedAction + + ${resetcolor} + } + if ($err.PSMessageDetails) { $posmsg = "${errorColor}${posmsg}" } - return $posmsg + return $posmsg + $recommendedAction } $indent = 4 @@ -330,6 +299,4 @@ $finalMsg = if ($err.ErrorDetails.Message) { $err.Exception.Message + $posmsg } -"${errorColor}${finalMsg}${resetcolor}" - -#> \ No newline at end of file +"${errorColor}${finalMsg}${resetcolor}" \ No newline at end of file diff --git a/Reference/detail copy.ps1 b/Reference/detail copy.ps1 deleted file mode 100644 index 79e9c36..0000000 --- a/Reference/detail copy.ps1 +++ /dev/null @@ -1,183 +0,0 @@ -<# -[int]$maxDepth = 10 -Set-StrictMode -Off - -$ellipsis = "`u{2026}" -$resetColor = '' -$errorColor = '' -$accentColor = '' - -if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - $resetColor = $PSStyle.Reset - $errorColor = $psstyle.Formatting.Error - $accentColor = $PSStyle.Formatting.FormatAccent -} - -function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) { - $newline = [Environment]::Newline - $output = [System.Text.StringBuilder]::new() - $prefix = ' ' * $indent - - $expandTypes = @( - 'Microsoft.Rest.HttpRequestMessageWrapper' - 'Microsoft.Rest.HttpResponseMessageWrapper' - 'System.Management.Automation.InvocationInfo' - ) - - # if object is an Exception, add an ExceptionType property - if ($obj -is [Exception]) { - $obj | Add-Member -NotePropertyName Type -NotePropertyValue $obj.GetType().FullName -ErrorAction Ignore - } - - # first find the longest property so we can indent properly - $propLength = 0 - foreach ($prop in $obj.PSObject.Properties) { - if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { - $propLength = $prop.Name.Length - } - } - - $addedProperty = $false - foreach ($prop in $obj.PSObject.Properties) { - - # don't show empty properties or our added property for $error[index] - if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { - $addedProperty = $true - $null = $output.Append($prefix) - $null = $output.Append($accentColor) - $null = $output.Append($prop.Name) - $propNameIndent = ' ' * ($propLength - $prop.Name.Length) - $null = $output.Append($propNameIndent) - $null = $output.Append(' : ') - $null = $output.Append($resetColor) - - $newIndent = $indent + 4 - - # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes - if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or - $expandTypes -contains $prop.TypeNameOfValue -or ($prop.TypeNames -ne $null -and $expandTypes -contains $prop.TypeNames[0])) { - - if ($depth -ge $maxDepth) { - $null = $output.Append($ellipsis) - } - else { - $null = $output.Append($newline) - $null = $output.Append((Show-ErrorRecord $prop.Value $newIndent ($depth + 1))) - } - } - # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members - elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { - if ($depth -ge $maxDepth) { - $null = $output.Append($ellipsis) - } - else { - $targetSite = [PSCustomObject]@{ - Name = $prop.Value.Name - DeclaringType = $prop.Value.DeclaringType - MemberType = $prop.Value.MemberType - Module = $prop.Value.Module - } - - $null = $output.Append($newline) - $null = $output.Append((Show-ErrorRecord $targetSite $newIndent ($depth + 1))) - } - } - # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation - elseif ($prop.Name -eq 'StackTrace') { - # for a stacktrace which is usually quite wide with info, we left justify it - $null = $output.Append($newline) - $null = $output.Append($prop.Value) - } - # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here - elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { - $isFirstElement = $true - foreach ($key in $prop.Value.Keys) { - if ($isFirstElement) { - $null = $output.Append($newline) - } - - if ($key -eq 'Authorization') { - $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}${ellipsis}${newline}") - } - else { - $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}$($prop.Value[$key])${newline}") - } - - $isFirstElement = $false - } - } - # if the object implements IEnumerable and not a string, we try to show each object - # We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here - elseif (!($prop.Value -is [System.String]) -and $prop.Value.GetType().GetInterface('IEnumerable') -ne $null -and $prop.Name -ne 'Data') { - - if ($depth -ge $maxDepth) { - $null = $output.Append($ellipsis) - } - else { - $isFirstElement = $true - foreach ($value in $prop.Value) { - $null = $output.Append($newline) - if (!$isFirstElement) { - $null = $output.Append($newline) - } - $null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1))) - $isFirstElement = $false - } - } - } - # Anything else, we convert to string. - # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error - else { - $value = $null - if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $value -ne $null) - { - if ($prop.Name -eq 'PositionMessage') { - $value = $value.Insert($value.IndexOf('~'), $errorColor) - } - elseif ($prop.Name -eq 'Message') { - $value = $errorColor + $value - } - - $isFirstLine = $true - if ($value.Contains($newline)) { - # the 3 is to account for ' : ' - $valueIndent = ' ' * ($propLength + 3) - # need to trim any extra whitespace already in the text - foreach ($line in $value.Split($newline)) { - if (!$isFirstLine) { - $null = $output.Append("${newline}${prefix}${valueIndent}") - } - $null = $output.Append($line.Trim()) - $isFirstLine = $false - } - } - else { - $null = $output.Append($value) - } - } - } - - $null = $output.Append($newline) - } - } - - # if we had added nested properties, we need to remove the last newline - if ($addedProperty) { - $null = $output.Remove($output.Length - $newline.Length, $newline.Length) - } - - $output.ToString() -} - -# Add back original typename and remove PSExtendedError -if ($_.PSObject.TypeNames.Contains('System.Management.Automation.ErrorRecord#PSExtendedError')) { - $_.PSObject.TypeNames.Add('System.Management.Automation.ErrorRecord') - $null = $_.PSObject.TypeNames.Remove('System.Management.Automation.ErrorRecord#PSExtendedError') -} -elseif ($_.PSObject.TypeNames.Contains('System.Exception#PSExtendedError')) { - $_.PSObject.TypeNames.Add('System.Exception') - $null = $_.PSObject.TypeNames.Remove('System.Exception#PSExtendedError') -} - -Show-ErrorRecord $_ -#> \ No newline at end of file diff --git a/Reference/detail.ps1 b/Reference/detail.ps1 index 79e9c36..f4f1ac8 100644 --- a/Reference/detail.ps1 +++ b/Reference/detail.ps1 @@ -1,7 +1,6 @@ -<# -[int]$maxDepth = 10 Set-StrictMode -Off +$maxDepth = 10 $ellipsis = "`u{2026}" $resetColor = '' $errorColor = '' @@ -117,14 +116,29 @@ function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) { $isFirstElement = $true foreach ($value in $prop.Value) { $null = $output.Append($newline) - if (!$isFirstElement) { - $null = $output.Append($newline) + $valueIndent = ' ' * ($newIndent + 2) + + if ($value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("${prefix}${valueIndent}[$($value.ToString())]") + } + elseif ($value -is [string] -or $value.GetType().IsPrimitive) { + $null = $output.Append("${prefix}${valueIndent}${value}") + } + else { + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1))) } - $null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1))) $isFirstElement = $false } } } + elseif ($prop.Value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("[$($prop.Value.ToString())]") + } # Anything else, we convert to string. # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error else { @@ -179,5 +193,4 @@ elseif ($_.PSObject.TypeNames.Contains('System.Exception#PSExtendedError')) { $null = $_.PSObject.TypeNames.Remove('System.Exception#PSExtendedError') } -Show-ErrorRecord $_ -#> \ No newline at end of file +Show-ErrorRecord $_ \ No newline at end of file diff --git a/Reference/legacy.ps1 b/Reference/legacy.ps1 new file mode 100644 index 0000000..8ab5ee2 --- /dev/null +++ b/Reference/legacy.ps1 @@ -0,0 +1,57 @@ +if ($_.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { + $_.Exception.Message +} +else +{ + $myinv = $_.InvocationInfo + if ($myinv -and ($myinv.MyCommand -or ($_.CategoryInfo.Category -ne 'ParserError'))) { + $posmsg = $myinv.PositionMessage + } else { + $posmsg = "" + } + + if ($posmsg -ne "") + { + $posmsg = "`n" + $posmsg + } + + if ( & { Set-StrictMode -Version 1; $_.PSMessageDetails } ) { + $posmsg = " : " + $_.PSMessageDetails + $posmsg + } + + $indent = 4 + $width = $host.UI.RawUI.BufferSize.Width - $indent - 2 + + $errorCategoryMsg = & { Set-StrictMode -Version 1; $_.ErrorCategory_Message } + if ($errorCategoryMsg -ne $null) + { + $indentString = "+ CategoryInfo : " + $_.ErrorCategory_Message + } + else + { + $indentString = "+ CategoryInfo : " + $_.CategoryInfo + } + $posmsg += "`n" + foreach($line in @($indentString -split "(.{$width})")) { if($line) { $posmsg += (" " * $indent + $line) } } + + $indentString = "+ FullyQualifiedErrorId : " + $_.FullyQualifiedErrorId + $posmsg += "`n" + foreach($line in @($indentString -split "(.{$width})")) { if($line) { $posmsg += (" " * $indent + $line) } } + + $originInfo = & { Set-StrictMode -Version 1; $_.OriginInfo } + if (($originInfo -ne $null) -and ($originInfo.PSComputerName -ne $null)) + { + $indentString = "+ PSComputerName : " + $originInfo.PSComputerName + $posmsg += "`n" + foreach($line in @($indentString -split "(.{$width})")) { if($line) { $posmsg += (" " * $indent + $line) } } + } + + if ($ErrorView -eq "CategoryView") { + $_.CategoryInfo.GetMessage() + } + elseif (! $_.ErrorDetails -or ! $_.ErrorDetails.Message) { + $_.Exception.Message + $posmsg + "`n " + } else { + $_.ErrorDetails.Message + $posmsg + } +} \ No newline at end of file diff --git a/source/ErrorView.format.ps1xml b/source/ErrorView.format.ps1xml index 028b7b5..582f7f0 100644 --- a/source/ErrorView.format.ps1xml +++ b/source/ErrorView.format.ps1xml @@ -11,12 +11,13 @@ <CustomEntries> <CustomEntry> <CustomItem> - <ExpressionBinding> - <ScriptBlock>Write-NativeCommandError $_</ScriptBlock> - </ExpressionBinding> <ExpressionBinding> <ScriptBlock> <![CDATA[ + Set-StrictMode -Off + $ErrorActionPreference = 'Stop' + trap { 'Error found in error view definition: ' + $_.Exception.Message } + if ($formatter = @(Get-Command "ConvertTo-$($ErrorView -replace "View$")ErrorView" -ListImported -ErrorAction Ignore -ParameterName InputObject -ParameterType [System.Management.Automation.ErrorRecord])) { & ($formatter[0]) -InputObject $_ } else { @@ -40,12 +41,13 @@ <CustomEntries> <CustomEntry> <CustomItem> - <ExpressionBinding> - <ScriptBlock>Write-NativeCommandError $_</ScriptBlock> - </ExpressionBinding> <ExpressionBinding> <ScriptBlock> <![CDATA[ + Set-StrictMode -Off + $ErrorActionPreference = 'Stop' + trap { 'Error found in error view definition: ' + $_.Exception.Message } + if ($_.ErrorRecord) { $Record = $_.ErrorRecord if ($formatter = @(Get-Command "ConvertTo-$($ErrorView -replace "View$")ErrorView" -ListImported -ErrorAction Ignore -ParameterName InputObject -ParameterType [System.Management.Automation.ErrorRecord])) { diff --git a/source/prefix.ps1 b/source/prefix.ps1 index aef18c4..5ab3ea2 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -5,10 +5,12 @@ param( ) # We need to _overwrite_ the ErrorView -# So -PrependPath, instead of FormatsToProcess +# So -PrependPath, instead of using FormatsToProcess Update-FormatData -PrependPath $PSScriptRoot\ErrorView.format.ps1xml Set-StrictMode -Off +$ErrorActionPreference = 'Stop' +trap { 'Error found in error view definition: ' + $_.Exception.Message } # Borrowed this one from https://github.com/chalk/ansi-regex $script:AnsiEscapes = [Regex]::new("([\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?(?:\u001b\u005c|\u0007))|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])))", "Compiled"); @@ -23,31 +25,9 @@ $script:AnsiEscapes = [Regex]::new("([\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-z # ESC O S # $script:AnsiEscapes = [Regex]::new("\x1b[\(\)%`"&\.\/*+.-][@-Z]|\x1b\].*?(?:\u001b\u005c|\u0007|^)|\x1b\[\P{L}*[@-_A-Za-z^`\{\|\}~]|\x1b#\d|\x1b[!-~]", [System.Text.RegularExpressions.RegexOptions]::Compiled); - - - - $script:ellipsis = [char]0x2026 $script:newline = [Environment]::Newline -$script:resetColor = '' -$script:errorColor = '' -$script:accentColor = '' -$script:errorAccentColor = '' $script:LineColors = @( "`e[38;2;255;255;255m" "`e[38;2;179;179;179m" ) - -if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - if ($PSStyle) { - $script:resetColor = $PSStyle.Reset - $script:errorColor = $PSStyle.Formatting.Error - $script:accentColor = $PSStyle.Formatting.FormatAccent - $script:errorAccentColor = $PSStyle.Formatting.ErrorAccent - } else { - $script:resetColor = "$([char]27)[0m" - $script:errorColor = "$([char]27)[31m" - $script:accentColor = "$([char]27)[32;1m" - $script:errorAccentColor = "$([char]27)[31;1m" - } -} \ No newline at end of file diff --git a/source/private/Recolor.ps1 b/source/private/Recolor.ps1 new file mode 100644 index 0000000..4a831a5 --- /dev/null +++ b/source/private/Recolor.ps1 @@ -0,0 +1,11 @@ +function ResetColor { + $script:resetColor = '' + $script:errorColor = '' + $script:accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $script:resetColor = "$([char]27)[0m" + $script:errorColor = if ($null -ne $PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } + $script:accentColor = if ($null -ne $PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } + } +} \ No newline at end of file diff --git a/source/private/WrapString.ps1 b/source/private/WrapString.ps1 index 4d3fbfa..26c2f52 100644 --- a/source/private/WrapString.ps1 +++ b/source/private/WrapString.ps1 @@ -36,84 +36,44 @@ filter WrapString { # $wrappableChars = [char[]]" ,.?!:;-`n`r`t" # $maxLength = $width - $IndentPadding.Length -1 $wrapper = [Regex]::new("((?:$AnsiPattern)*[^-=,.?!:;\s\r\n\t\\\/\|]+(?:$AnsiPattern)*)", "Compiled") - $output = [System.Text.StringBuilder]::new($Colors[$color] + $IndentPadding) + $output = [System.Text.StringBuilder]::new() + $buffer = [System.Text.StringBuilder]::new($Colors[$color]) + $lineLength = 0 + if ($Width -lt $IndentPadding.Length) { + Write-Warning "Width $Width is less than IndentPadding length $($IndentPadding.Length). Setting Width to BufferWidth ($($Host.UI.RawUI.BufferSize.Width))" + } } process { foreach($line in $InputObject -split "(\r?\n)") { # Don't bother trying to split empty lines if ([String]::IsNullOrWhiteSpace($AnsiRegex.Replace($line, ''))) { Write-Debug "Empty String ($($line.Length))" - if ($EmphasizeOriginalNewlines) { [string]::Empty } + if ($EmphasizeOriginalNewlines) { + $null = $output.Append($newline) + } continue } $slices = $line -split $wrapper | ForEach-Object { @{ Text = $_; Length = MeasureString $_ } } Write-Debug "$($line.Length) words in line. $($AnsiRegex.Replace($line, ''))" - $lineLength = $IndentPadding.Length foreach($slice in $slices) { $lineLength += $slice.Length if ($lineLength -le $Width) { Write-Verbose "+ $($slice.Length) = $lineLength < $Width" - $null = $output.Append($slice.Text) + $null = $buffer.Append($slice.Text) } else { Write-Verbose "Output $($lineLength - $slice.Length)" Write-Verbose "+ $($slice.Length) = $($slice.Length)" $color = ($color + 1) % $Colors.Length - $output.ToString().Trim() - $null = $output.Clear().Append($Colors[$color]).Append($IndentPadding).Append($slice.Text) + #$null = $output.Append($buffer.ToString()) + $null = $buffer.Append($newline).Append($slice.Text) $lineLength = $IndentPadding.Length + $slice.Length } } - $output.ToString().Trim() - $null = $output.Clear().Append($Colors[$color]).Append($IndentPadding) + $null = $output.Append($buffer.ToString()) + $null = $buffer.Clear().Append($newline).Append($Colors[$color]).Append($IndentPadding) + $lineLength = $IndentPadding.Length } - - # $currentIndex = 0; - # $lastWrap = 0; - # do { - # $lastWrap = $currentIndex; - # #Write-Verbose "m: $Width, l: $($line.Length), c: $color $($Colors[$color] -replace "`e","``e")" - - - - # if ($AnsiEscapes.Replace($first, '').Length -gt $maxLength + 1) { - # # If we couldn't find a good place to wrap, just wrap at the end of the line - # $first = $line.Substring(0, $maxLength+1) - # $line = $line.Substring($maxLength+1) - # } - # $Colors[$color] + $IndentPadding + $first.TrimEnd() - -<# $currentIndex = $( - if ($lastWrap + $Width -gt $line.Length) { - $line.Length - } else { - $line.LastIndexOfAny($wrappableChars, ([Math]::Min(($line.Length - 1), ($lastWrap + $Width)))) + 1 - } - ) - - $slice = $line.Substring($lastWrap, ($currentIndex - $lastWrap)) - if (($slice.Length - $script:AnsiEscapes.Replace($slice,'').Length) -gt 0) { - $currentIndex = $( - if ($lastWrap + $Width -gt $line.Length) { - $line.Length - } else { - $line.LastIndexOfAny($wrappableChars, ([Math]::Min(($line.Length - 1), ($lastWrap + $Width)))) + 1 - } - ) - } - - # If we couldn't find a good place to wrap, just wrap at the end of the line - if ($currentIndex -le $lastWrap) { - $currentIndex = [Math]::Min(($lastWrap + $Width), $line.Length ) - } - # Output the line, with the appropriate color and padding - $Colors[$color] + $IndentPadding + $line.Substring($lastWrap, ($currentIndex - $lastWrap)).TrimEnd() -#> -<# } while($line); - - if ($line -ne "`n") { - $color = ($color + 1) % $Colors.Length - } - } #> + $output.ToString() } } \ No newline at end of file diff --git a/source/public/ConvertTo-NormalErrorView.ps1 b/source/public/ConvertTo-NormalErrorView.ps1 index a33dc72..70a6da5 100644 --- a/source/public/ConvertTo-NormalErrorView.ps1 +++ b/source/public/ConvertTo-NormalErrorView.ps1 @@ -11,64 +11,49 @@ filter ConvertTo-NormalErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) - - if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { - $errorColor + $InputObject.Exception.Message + $resetColor - } else { - $myinv = $InputObject.InvocationInfo - if ($myinv -and ($myinv.MyCommand -or ($InputObject.CategoryInfo.Category -ne 'ParserError'))) { - $posmsg = $myinv.PositionMessage + begin { ResetColor } + process { + if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { + "${errorColor}$($InputObject.Exception.Message)${resetColor}" } else { - $posmsg = "" - } - - if ($posmsg -ne "") { - $posmsg = "`n" + $posmsg - } - - if ( &{ Set-StrictMode -Version 1; $InputObject.PSMessageDetails } ) { - $posmsg = " : " + $InputObject.PSMessageDetails + $posmsg - } + $myinv = $InputObject.InvocationInfo + $posmsg = '' + if ($myinv -and ($myinv.MyCommand -or ($InputObject.CategoryInfo.Category -ne 'ParserError')) -and $myinv.PositionMessage) { + $posmsg = $newline + $myinv.PositionMessage + } - $indent = 4 - $width = $host.UI.RawUI.BufferSize.Width - $indent - 2 + if ($err.PSMessageDetails) { + $posmsg = ' : ' + $err.PSMessageDetails + $posmsg + } - $errorCategoryMsg = &{ Set-StrictMode -Version 1; $InputObject.ErrorCategory_Message } - if ($null -ne $errorCategoryMsg) { - $indentString = $accentColor + "+ CategoryInfo : " + $resetColor + $InputObject.ErrorCategory_Message - } else { - $indentString = $accentColor + "+ CategoryInfo : " + $resetColor + $InputObject.CategoryInfo - } - $posmsg += "`n" - foreach ($line in @($indentString -split "(.{$width})")) { - if ($line) { - $posmsg += (" " * $indent + $line) + $Wrap = @{ + Width = $width + IndentPadding = " " } - } + $width = $host.UI.RawUI.BufferSize.Width - 2 - $indentString = $accentColor + "+ FullyQualifiedErrorId: " + $resetColor + $InputObject.FullyQualifiedErrorId - $posmsg += "`n" - foreach ($line in @($indentString -split "(.{$width})")) { - if ($line) { - $posmsg += (" " * $indent + $line) + $errorCategoryMsg = $InputObject.ErrorCategory_Message + [string]$line = if ($null -ne $errorCategoryMsg) { + $accentColor + "+ CategoryInfo : " + $errorColor + $InputObject.ErrorCategory_Message | WrapString @Wrap + } else { + $accentColor + "+ CategoryInfo : " + $errorColor + $InputObject.CategoryInfo | WrapString @Wrap } - } + $posmsg += $newline + $line - $originInfo = &{ Set-StrictMode -Version 1; $InputObject.OriginInfo } - if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) { - $indentString = "+ PSComputerName : " + $originInfo.PSComputerName - $posmsg += "`n" - foreach ($line in @($indentString -split "(.{$width})")) { - if ($line) { - $posmsg += (" " * $indent + $line) - } + $line = $accentColor + "+ FullyQualifiedErrorId: " + $errorColor + $InputObject.FullyQualifiedErrorId | WrapString @Wrap + $posmsg += $newline + $line + + $originInfo = $InputObject.OriginInfo + if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) { + $line = $accentColor + "+ PSComputerName : " + $errorColor + $originInfo.PSComputerName | WrapString @Wrap + $posmsg += $newline + $line } - } - if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { - $errorColor + $InputObject.Exception.Message + $resetColor + $posmsg + "`n " - } else { - $errorColor + $InputObject.ErrorDetails.Message + $resetColor + $posmsg + if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { + $errorColor + $InputObject.Exception.Message + $posmsg + $resetColor + } else { + $errorColor + $InputObject.ErrorDetails.Message + $posmsg + $resetColor + } } } } \ No newline at end of file diff --git a/source/public/ConvertTo-NormalExceptionView.ps1 b/source/public/ConvertTo-NormalExceptionView.ps1 index 929b040..31de80f 100644 --- a/source/public/ConvertTo-NormalExceptionView.ps1 +++ b/source/public/ConvertTo-NormalExceptionView.ps1 @@ -11,16 +11,8 @@ filter ConvertTo-NormalExceptionView { [System.Exception] $InputObject ) - $resetColor = '' - $errorColor = '' - #$accentColor = '' - - if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - $resetColor = "$([char]0x1b)[0m" - $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } - #$accentColor = if ($PSStyle.Formatting.ErrorAccent) { $PSStyle.Formatting.ErrorAccent } else { "`e[1;36m" } + begin { ResetColor } + process { + $errorColor + $InputObject.Message + $resetColor } - - $errorColor + $InputObject.Message + $resetColor - } \ No newline at end of file diff --git a/source/public/Get-ErrorPrefix.ps1 b/source/public/Get-ErrorPrefix.ps1 new file mode 100644 index 0000000..cce9461 --- /dev/null +++ b/source/public/Get-ErrorPrefix.ps1 @@ -0,0 +1,46 @@ +filter Get-ErrorPrefix { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + $InputObject + ) + if (@('NativeCommandErrorMessage', 'NativeCommandError') -notcontains $_.FullyQualifiedErrorId) { + if ($InputObject -is [System.Exception]) { + $InputObject.GetType().FullName + " : " + } else { + $myinv = $InputObject.InvocationInfo + if ($myinv -and $myinv.MyCommand) { + switch -regex ( $myinv.MyCommand.CommandType ) { + ([System.Management.Automation.CommandTypes]::ExternalScript) { + if ($myinv.MyCommand.Path) { + $myinv.MyCommand.Path + ' : ' + } + + break + } + + ([System.Management.Automation.CommandTypes]::Script) { + if ($myinv.MyCommand.ScriptBlock) { + $myinv.MyCommand.ScriptBlock.ToString() + ' : ' + } + + break + } + default { + if ($myinv.InvocationName -match '^[&\.]?$') { + if ($myinv.MyCommand.Name) { + $myinv.MyCommand.Name + ' : ' + } + } else { + $myinv.InvocationName + ' : ' + } + + break + } + } + } elseif ($myinv -and $myinv.InvocationName) { + $myinv.InvocationName + ' : ' + } + } + } +} \ No newline at end of file diff --git a/source/public/Write-NativeCommandError.ps1 b/source/public/Write-NativeCommandError.ps1 deleted file mode 100644 index 5ab8889..0000000 --- a/source/public/Write-NativeCommandError.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -function Write-NativeCommandError { - [CmdletBinding()] - param( - $InputObject - ) - $resetColor = '' - $errorColor = '' - $accentColor = '' - - if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { - $resetColor = "$([char]0x1b)[0m" - $errorColor = if ($PSStyle.Formatting.Error) { $PSStyle.Formatting.Error } else { "`e[1;31m" } - $accentColor = $PSStyle.Formatting.ErrorAccent - } - if ($InputObject -is [System.Exception]) { - $errorColor + $InputObject.GetType().FullName + " : " + $resetColor - } - - # @('NativeCommandErrorMessage', 'NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView', 'ConciseView', 'DetailedView') -notcontains $ErrorView - if (@('NativeCommandErrorMessage', 'NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView', 'ConciseView', 'DetailedView') -notcontains $ErrorView) { - $invoc = $InputObject.InvocationInfo - if ($invoc -and $invoc.MyCommand) { - switch -regex ( $invoc.MyCommand.CommandType ) { - ([System.Management.Automation.CommandTypes]::ExternalScript) { - if ($invoc.MyCommand.Path) { - $errorColor + $invoc.MyCommand.Path + " : " + $resetColor - } - break - } - ([System.Management.Automation.CommandTypes]::Script) { - if ($invoc.MyCommand.ScriptBlock) { - $errorColor + $invoc.MyCommand.ScriptBlock.ToString() + " : " + $resetColor - } - break - } - default { - if ($invoc.InvocationName -match '^[&\.]?$') { - if ($invoc.MyCommand.Name) { - $errorColor + $invoc.MyCommand.Name + " : " + $resetColor - } - } else { - $errorColor + $invoc.InvocationName + " : " + $resetColor - } - break - } - } - } elseif ($invoc -and $invoc.InvocationName) { - $errorColor + $invoc.InvocationName + " : " + $resetColor - } - } -} \ No newline at end of file From f9abe85d43fc8525161ab81d006e0180ee42bbfd Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Fri, 27 Sep 2024 22:52:55 -0400 Subject: [PATCH 04/15] This is ConciseView --- .../private/GetConciseViewPositionMessage.ps1 | 176 ++++++++++++++++++ source/public/ConvertTo-ConciseErrorView.ps1 | 64 ++----- 2 files changed, 193 insertions(+), 47 deletions(-) create mode 100644 source/private/GetConciseViewPositionMessage.ps1 diff --git a/source/private/GetConciseViewPositionMessage.ps1 b/source/private/GetConciseViewPositionMessage.ps1 new file mode 100644 index 0000000..7acdaac --- /dev/null +++ b/source/private/GetConciseViewPositionMessage.ps1 @@ -0,0 +1,176 @@ +filter GetConciseViewPositionMessage { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + $err = $InputObject + $posmsg = '' + $headerWhitespace = '' + $offsetWhitespace = '' + $message = '' + $prefix = '' + + # Handle case where there is a TargetObject from a Pester `Should` assertion failure and we can show the error at the target rather than the script source + # Note that in some versions, this is a Dictionary<,> and in others it's a hashtable. So we explicitly cast to a shared interface in the method invocation + # to force using `IDictionary.Contains`. Hashtable does have it's own `ContainKeys` as well, but if they ever opt to use a custom `IDictionary`, that may not. + $useTargetObject = $null -ne $err.TargetObject -and + $err.TargetObject -is [System.Collections.IDictionary] -and + ([System.Collections.IDictionary]$err.TargetObject).Contains('Line') -and + ([System.Collections.IDictionary]$err.TargetObject).Contains('LineText') + + # The checks here determine if we show line detailed error information: + # - check if `ParserError` and comes from PowerShell which eventually results in a ParseException, but during this execution it's an ErrorRecord + $isParseError = $err.CategoryInfo.Category -eq 'ParserError' -and + $err.Exception -is [System.Management.Automation.ParentContainsErrorRecordException] + + # - check if invocation is a script or multiple lines in the console + $isMultiLineOrExternal = $myinv.ScriptName -or $myinv.ScriptLineNumber -gt 1 + + # - check that it's not a script module as expectation is that users don't want to see the line of error within a module + $shouldShowLineDetail = ($isParseError -or $isMultiLineOrExternal) -and + $myinv.ScriptName -notmatch '\.psm1$' + + if ($useTargetObject -or $shouldShowLineDetail) { + + if ($useTargetObject) { + $posmsg = "${resetcolor}$($err.TargetObject.File)${newline}" + } elseif ($myinv.ScriptName) { + if ($env:TERM_PROGRAM -eq 'vscode') { + # If we are running in vscode, we know the file:line:col links are clickable so we use this format + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber):$($myinv.OffsetInLine)${newline}" + } else { + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber)${newline}" + } + } else { + $posmsg = "${newline}" + } + + if ($useTargetObject) { + $scriptLineNumber = $err.TargetObject.Line + $scriptLineNumberLength = $err.TargetObject.Line.ToString().Length + } else { + $scriptLineNumber = $myinv.ScriptLineNumber + $scriptLineNumberLength = $myinv.ScriptLineNumber.ToString().Length + } + + if ($scriptLineNumberLength -gt 4) { + $headerWhitespace = ' ' * ($scriptLineNumberLength - 4) + } + + $lineWhitespace = '' + if ($scriptLineNumberLength -lt 4) { + $lineWhitespace = ' ' * (4 - $scriptLineNumberLength) + } + + $verticalBar = '|' + $posmsg += "${accentColor}${headerWhitespace}Line ${verticalBar}${newline}" + + $highlightLine = '' + if ($useTargetObject) { + $line = $_.TargetObject.LineText.Trim() + $offsetLength = 0 + $offsetInLine = 0 + } else { + $positionMessage = $myinv.PositionMessage.Split($newline) + $line = $positionMessage[1].Substring(1) # skip the '+' at the start + $highlightLine = $positionMessage[$positionMessage.Count - 1].Substring(1) + $offsetLength = $highlightLine.Trim().Length + $offsetInLine = $highlightLine.IndexOf('~') + } + + if (-not $line.EndsWith($newline)) { + $line += $newline + } + + # don't color the whole line + if ($offsetLength -lt $line.Length - 1) { + $line = $line.Insert($offsetInLine + $offsetLength, $resetColor).Insert($offsetInLine, $accentColor) + } + + $posmsg += "${accentColor}${lineWhitespace}${ScriptLineNumber} ${verticalBar} ${resetcolor}${line}" + $offsetWhitespace = ' ' * $offsetInLine + $prefix = "${accentColor}${headerWhitespace} ${verticalBar} ${errorColor}" + if ($highlightLine -ne '') { + $posMsg += "${prefix}${highlightLine}${newline}" + } + $message = "${prefix}" + } + + if (! $err.ErrorDetails -or ! $err.ErrorDetails.Message) { + if ($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception.Message.Contains("~$newline")) { + # need to parse out the relevant part of the pre-rendered positionmessage + $message += $err.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } elseif ($err.Exception) { + $message += $err.Exception.Message + } elseif ($err.Message) { + $message += $err.Message + } else { + $message += $err.ToString() + } + } else { + $message += $err.ErrorDetails.Message + } + + # if rendering line information, break up the message if it's wider than the console + if ($myinv -and $myinv.ScriptName -or $err.CategoryInfo.Category -eq 'ParserError') { + $prefixLength = [System.Management.Automation.Internal.StringDecorated]::new($prefix).ContentLength + $prefixVtLength = $prefix.Length - $prefixLength + + # replace newlines in message so it lines up correct + $message = $message.Replace($newline, ' ').Replace("`n", ' ').Replace("`t", ' ') + + $windowWidth = 120 + if ($Host.UI.RawUI -ne $null) { + $windowWidth = $Host.UI.RawUI.WindowSize.Width + } + + if ($windowWidth -gt 0 -and ($message.Length - $prefixVTLength) -gt $windowWidth) { + $sb = [Text.StringBuilder]::new() + $substring = TruncateString -string $message -length ($windowWidth + $prefixVTLength) + $null = $sb.Append($substring) + $remainingMessage = $message.Substring($substring.Length).Trim() + $null = $sb.Append($newline) + while (($remainingMessage.Length + $prefixLength) -gt $windowWidth) { + $subMessage = $prefix + $remainingMessage + $substring = TruncateString -string $subMessage -length ($windowWidth + $prefixVtLength) + + if ($substring.Length - $prefix.Length -gt 0) { + $null = $sb.Append($substring) + $null = $sb.Append($newline) + $remainingMessage = $remainingMessage.Substring($substring.Length - $prefix.Length).Trim() + } else { + break + } + } + $null = $sb.Append($prefix + $remainingMessage.Trim()) + $message = $sb.ToString() + } + + $message += $newline + } + + $posmsg += "${errorColor}" + $message + + $reason = 'Error' + if ($err.Exception -and $err.Exception.WasThrownFromThrowStatement) { + $reason = 'Exception' + # MyCommand can be the script block, so we don't want to show that so check if it's an actual command + } elseif ($myinv.MyCommand -and $myinv.MyCommand.Name -and (Get-Command -Name $myinv.MyCommand -ErrorAction Ignore)) { + $reason = $myinv.MyCommand + } elseif ($err.CategoryInfo.Activity) { + # If it's a scriptblock, better to show the command in the scriptblock that had the error + $reason = $err.CategoryInfo.Activity + } elseif ($myinv.MyCommand) { + $reason = $myinv.MyCommand + } elseif ($myinv.InvocationName) { + $reason = $myinv.InvocationName + } elseif ($err.CategoryInfo.Category) { + $reason = $err.CategoryInfo.Category + } elseif ($err.CategoryInfo.Reason) { + $reason = $err.CategoryInfo.Reason + } + + "${errorColor}${reason}: ${posmsg}${resetcolor}" +} diff --git a/source/public/ConvertTo-ConciseErrorView.ps1 b/source/public/ConvertTo-ConciseErrorView.ps1 index 391014f..f36b53b 100644 --- a/source/public/ConvertTo-ConciseErrorView.ps1 +++ b/source/public/ConvertTo-ConciseErrorView.ps1 @@ -1,62 +1,32 @@ function ConvertTo-ConciseErrorView { [CmdletBinding()] param( + [Parameter(ValueFromPipeline)] [System.Management.Automation.ErrorRecord] $InputObject ) - if ("$accentColor".Length) { - $local:accentColor = $script:errorAccentColor - $local:resetColor = $script:resetColor - } else { - $local:accentColor = ">>>" - $local:resetColor = "<<<" - } - - - if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { - $errorColor + $InputObject.Exception.Message + $resetColor - } else { - $myinv = $InputObject.InvocationInfo - if ($myinv -and ($myinv.MyCommand -or ($InputObject.CategoryInfo.Category -ne 'ParserError'))) { - # rip off lines that say "At line:1 char:1" (hopefully, in a language agnostic way) - $posmsg = $myinv.PositionMessage -replace "^At line:1 char:1[\r\n]+" - - # rip off the underline and use the accentcolor instead - $pattern = $posmsg -split "[\r\n]+" -match "\+( +~+)\s*" -replace '(~+)', '($1)' -replace '( +)','($1)' -replace '~| ','.' - $posmsg = $posmsg -replace '[\r\n]+\+ +~+' - if ($pattern) { - $posmsg = $posmsg -replace "\+$pattern", "+`$1$accentColor`$2$resetColor" - } + begin { ResetColor } + process { + if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { + "${errorColor}$($InputObject.Exception.Message)${resetColor}" } else { - $posmsg = "" - } - - if ($posmsg -ne "") { - $posmsg = "`n" + $posmsg - } + if (!"$accentColor".Length) { + $local:accentColor = ">>>" + $local:resetColor = "<<<" + } - if ( & { Set-StrictMode -Version 1; $InputObject.PSMessageDetails } ) { - $posmsg = " : " + $InputObject.PSMessageDetails + $posmsg - } + $message = GetConciseViewPositionMessage -InputObject $InputObject - $indent = 4 - $width = $host.UI.RawUI.BufferSize.Width - $indent - 2 + if ($InputObject.PSMessageDetails) { + $message = $errorColor + ' : ' + $InputObject.PSMessageDetails + $message + } - $originInfo = & { Set-StrictMode -Version 1; $InputObject.OriginInfo } - if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) { - $indentString = "+ PSComputerName : " + $originInfo.PSComputerName - $posmsg += "`n" - foreach ($line in @($indentString -split "(.{$width})")) { - if ($line) { - $posmsg += (" " * $indent + $line) - } + $recommendedAction = $InputObject.ErrorDetails.RecommendedAction + if (-not [String]::IsNullOrWhiteSpace($recommendedAction)) { + $message = $message + $newline + ${errorColor} + ' Recommendation: ' + $recommendedAction + ${resetcolor} } - } - if (!$InputObject.ErrorDetails -or !$InputObject.ErrorDetails.Message) { - $errorColor + $InputObject.Exception.Message + $resetColor + $posmsg + "`n " - } else { - $errorColor + $InputObject.ErrorDetails.Message + $resetColor + $posmsg + $message } } } \ No newline at end of file From f20497cdc4ca595bb3223cb35e3c698de8508cc2 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Fri, 27 Sep 2024 22:53:41 -0400 Subject: [PATCH 05/15] Start by fixing the views. This is NormalView --- source/public/ConvertTo-NormalErrorView.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/public/ConvertTo-NormalErrorView.ps1 b/source/public/ConvertTo-NormalErrorView.ps1 index 70a6da5..9ee9113 100644 --- a/source/public/ConvertTo-NormalErrorView.ps1 +++ b/source/public/ConvertTo-NormalErrorView.ps1 @@ -27,10 +27,9 @@ filter ConvertTo-NormalErrorView { } $Wrap = @{ - Width = $width + Width = $host.UI.RawUI.BufferSize.Width - 2 IndentPadding = " " } - $width = $host.UI.RawUI.BufferSize.Width - 2 $errorCategoryMsg = $InputObject.ErrorCategory_Message [string]$line = if ($null -ne $errorCategoryMsg) { From 771e1db9f7aeb9a597f0a914238904cf07f95cd2 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Sat, 28 Sep 2024 14:46:10 -0400 Subject: [PATCH 06/15] This is DetailedView --- source/private/GetListRecursive.ps1 | 116 +++++++++--------- source/public/ConvertTo-DetailedErrorView.ps1 | 17 +-- 2 files changed, 64 insertions(+), 69 deletions(-) diff --git a/source/private/GetListRecursive.ps1 b/source/private/GetListRecursive.ps1 index a6cdd05..0b3e4d2 100644 --- a/source/private/GetListRecursive.ps1 +++ b/source/private/GetListRecursive.ps1 @@ -9,10 +9,8 @@ function GetListRecursive { [int]$indent = 0, [int]$depth = 1 ) - Write-Information "ENTER GetListRecursive END $($InputObject.GetType().FullName) $indent $depth" -Tags 'Trace', 'Enter', 'GetListRecursive' - Write-Information (Get-PSCallStack) -Tags 'Trace', 'StackTrace', 'GetListRecursive' $output = [System.Text.StringBuilder]::new() - $prefix = ' ' * $indent + $padding = ' ' * $indent $expandTypes = @( 'Microsoft.Rest.HttpRequestMessageWrapper' @@ -20,29 +18,29 @@ function GetListRecursive { 'System.Management.Automation.InvocationInfo' ) - # The built-in DetailedView aligns all the ":" characters, so we need to find the longest property name - $propLength = 0 - foreach ($prop in $InputObject.PSObject.Properties) { - if ($null -ne $prop.Value -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { - $propLength = $prop.Name.Length - } - } + # The built-in DetailedView aligns all the ":" characters, but it's awful $addedProperty = $false foreach ($prop in $InputObject.PSObject.Properties) { + # PowerShell creates an ErrorRecord property on Exceptions that points back to the parent ErrorRecord. + # This is basically a circular reference that causes repeated informtion, so we're going to skip them + if ($prop.Value -is [System.Management.Automation.ErrorRecord] -and $depth -ge 2) { + continue + } # don't show empty properties or our added property for $error[index] if ($null -ne $prop.Value -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { $addedProperty = $true - $null = $output.Append($prefix) + $null = $output.Append($padding) $null = $output.Append($accentColor) $null = $output.Append($prop.Name) - $null = $output.Append(' ',($propLength - $prop.Name.Length)) - $null = $output.Append(' : ') + $null = $output.Append(': ') $null = $output.Append($resetColor) - $newIndent = $indent + 2 + [int]$nextIndent = $indent + 2 + [int]$nextDepth = $depth + 1 + $nextPadding = ' ' * $nextIndent - # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes + # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or $expandTypes -contains $prop.TypeNameOfValue -or @@ -51,16 +49,12 @@ function GetListRecursive { if ($depth -ge $maxDepth) { $null = $output.Append($ellipsis) } else { + # For Exceptions, add a fake "Type" property if ($prop.Value -is [Exception]) { - $null = $output.Append($newline) - $null = $output.Append(( - GetListRecursive ([PSCustomObject]@{ - "Type" = $errorAccentColor + $prop.Value.GetType().FullName + $resetColor - }) $newIndent ($depth + 1) - )) + $null = $output.Append(( $accentColor + "[" + $prop.Value.GetType().FullName + "]" + $resetColor)) } $null = $output.Append($newline) - $null = $output.Append((GetListRecursive $prop.Value $newIndent ($depth + 1))) + $null = $output.Append((GetListRecursive $prop.Value $nextIndent $nextDepth)) } } elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members @@ -75,28 +69,34 @@ function GetListRecursive { } $null = $output.Append($newline) - $null = $output.Append((GetListRecursive $targetSite $newIndent ($depth + 1))) + $null = $output.Append((GetListRecursive $targetSite $nextIndent $nextDepth)) } } elseif ($prop.Name -eq 'StackTrace') { - # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation - # for a stacktrace which is usually quite wide with info, we left justify it + # StackTrace is handled specifically because the lines are typically long but we can't trucate them, so we don't indent it any more $null = $output.Append($newline) - $null = $output.Append($prop.Value) + # $null = $output.Append($prop.Value) + $Wrap = @{ + Width = $Host.UI.RawUI.BufferSize.Width - 2 + IndentPadding = "" + HangingIndent = " " + } + $null = $output.Append(($prop.Value | WrapString @Wrap)) + } elseif ($prop.Name -eq 'HResult') { + # `HResult` is handled specifically so we can format it in hex + # $null = $output.Append($newline) + $null = $output.Append("0x{0:x} ({0})" -f $prop.Value) + } elseif ($prop.Name -eq 'PipelineIterationInfo') { + # I literally have no idea what use this is + $null = $output.Append($prop.Value -join ', ') } elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { - # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here - $isFirstElement = $true + # Dictionary and Hashtable we want to show as Key/Value pairs + $null = $output.Append($newline) foreach ($key in $prop.Value.Keys) { - if ($isFirstElement) { - $null = $output.Append($newline) - } - if ($key -eq 'Authorization') { - $null = $output.Append("${prefix} ${accentColor}${key}: ${resetColor}${ellipsis}${newline}") + $null = $output.Append("${nextPadding}${accentColor}${key}: ${resetColor}${ellipsis}${newline}") } else { - $null = $output.Append("${prefix} ${accentColor}${key}: ${resetColor}$($prop.Value[$key])${newline}") + $null = $output.Append("${nextPadding}${accentColor}${key}: ${resetColor}$($prop.Value[$key])${newline}") } - - $isFirstElement = $false } } elseif (!($prop.Value -is [System.String]) -and $null -ne $prop.Value.GetType().GetInterface('IEnumerable') -and $prop.Name -ne 'Data') { # if the object implements IEnumerable and not a string, we try to show each object @@ -108,41 +108,43 @@ function GetListRecursive { $isFirstElement = $true foreach ($value in $prop.Value) { $null = $output.Append($newline) - if (!$isFirstElement) { - $null = $output.Append($newline) + + if ($value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("${nextPadding}[$($value.ToString())]") + } elseif ($value -is [string] -or $value.GetType().IsPrimitive) { + $null = $output.Append("${nextPadding}${value}") + } else { + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((GetListRecursive $value $nextIndent $nextDepth)) } - $null = $output.Append((GetListRecursive $value $newIndent ($depth + 1))) $isFirstElement = $false } } + } elseif ($prop.Value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("[$($prop.Value.ToString())]") } else { # Anything else, we convert to string. # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error $value = $null if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $null -ne $value) { $value = $value.Trim() - if ($prop.Name -eq 'PositionMessage') { + if ($InputObject -is [System.Management.Automation.InvocationInfo] -and $prop.Name -eq 'PositionMessage') { + # Make the underline red $value = $value.Insert($value.IndexOf('~'), $errorColor) - } elseif ($prop.Name -eq 'Message') { + } elseif ( ($InputObject -is [System.Management.Automation.ErrorRecord] -or + $InputObject -is [System.Exception]) -and $prop.Name -in 'Message', 'FullyQualifiedErrorId', 'CategoryInfo') { $value = $errorColor + $value } - - $isFirstLine = $true - if ($value.Contains($newline)) { - # the 3 is to account for ' : ' - # $valueIndent = ' ' * ($prop.Name.Length + 2) - $valueIndent = ' ' * ($propLength + 3) - # need to trim any extra whitespace already in the text - foreach ($line in $value.Split($newline)) { - if (!$isFirstLine) { - $null = $output.Append("${newline}${prefix}${valueIndent}") - } - $null = $output.Append($line.Trim()) - $isFirstLine = $false - } - } else { - $null = $output.Append($value) + $Wrap = @{ + Width = $Host.UI.RawUI.BufferSize.Width - 2 + IndentPadding = " " * ($nextIndent + $prop.Name.Length) } + + $null = $output.Append(($value | WrapString @Wrap).TrimStart()) } } diff --git a/source/public/ConvertTo-DetailedErrorView.ps1 b/source/public/ConvertTo-DetailedErrorView.ps1 index 210520c..555ba16 100644 --- a/source/public/ConvertTo-DetailedErrorView.ps1 +++ b/source/public/ConvertTo-DetailedErrorView.ps1 @@ -1,10 +1,10 @@ -function ConvertTo-DetailedErrorView { +filter ConvertTo-DetailedErrorView { <# .SYNOPSIS Converts an ErrorRecord to a detailed error string .DESCRIPTION - The default PowerShell "DetailedView" ErrorView - Copied from the PowerShellCore.format.ps1xml + An "improved" version of the PowerShell "DetailedView" ErrorView + Originally copied from the PowerShellCore.format.ps1xml .LINK https://github.com/PowerShell/PowerShell/blob/c444645b0941d73dc769f0bba6ab70d317bd51a9/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs#L903 #> @@ -19,15 +19,8 @@ function ConvertTo-DetailedErrorView { # The maximum depth to recurse into the object [int]$maxDepth = 10 ) - - begin { - Write-Information "ENTER ConvertTo-DetailedErrorView BEGIN " -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' - - Write-Information "EXIT ConvertTo-DetailedErrorView BEGIN" -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' - } + begin { ResetColor } process { - Write-Information "ENTER ConvertTo-DetailedErrorView PROCESS $($InputObject.GetType().FullName)" -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' - GetListRecursive $InputObject - Write-Information "EXIT ConvertTo-DetailedErrorView PROCESS $($InputObject.GetType().FullName)" -Tags 'Trace', 'Enter', 'ConvertTo-DetailedErrorView' + $newline + (GetListRecursive $InputObject) + $newline } } From 7618f00532bb7a94fdf7d94ae05678910b34619b Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 30 Sep 2024 01:32:28 -0400 Subject: [PATCH 07/15] Fix WrapString so it works with LineColors --- source/private/WrapString.ps1 | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/source/private/WrapString.ps1 b/source/private/WrapString.ps1 index 26c2f52..ed52337 100644 --- a/source/private/WrapString.ps1 +++ b/source/private/WrapString.ps1 @@ -20,10 +20,17 @@ filter WrapString { [Parameter(Position=0)] [int]$Width = ($Host.UI.RawUI.BufferSize.Width), - # The padding to add to the front of each line to cause indenting. Defaults to empty string. - [Parameter(Position=1)] + # The padding for each line defaults to an empty string. + # If set, whitespace on the front of each line is replaced with this string. [string]$IndentPadding = ([string]::Empty), + # If set, this will be used only for the first line (defaults to IndentPadding) + [string]$FirstLineIndent = $IndentPadding, + + # If set, wrapped lines use this instead of IndentPadding to create a hanging indent + [string]$WrappedIndent = $IndentPadding, + + # If set, colors to use for alternating lines [string[]]$Colors = @(''), @@ -31,13 +38,14 @@ filter WrapString { [switch]$EmphasizeOriginalNewlines ) begin { + $FirstLine = $true $color = 0; Write-Debug "Colors: $($Colors -replace "`e(.+)", "`e`$1``e`$1")" # $wrappableChars = [char[]]" ,.?!:;-`n`r`t" # $maxLength = $width - $IndentPadding.Length -1 $wrapper = [Regex]::new("((?:$AnsiPattern)*[^-=,.?!:;\s\r\n\t\\\/\|]+(?:$AnsiPattern)*)", "Compiled") $output = [System.Text.StringBuilder]::new() - $buffer = [System.Text.StringBuilder]::new($Colors[$color]) + $buffer = [System.Text.StringBuilder]::new() $lineLength = 0 if ($Width -lt $IndentPadding.Length) { Write-Warning "Width $Width is less than IndentPadding length $($IndentPadding.Length). Setting Width to BufferWidth ($($Host.UI.RawUI.BufferSize.Width))" @@ -45,6 +53,9 @@ filter WrapString { } process { foreach($line in $InputObject -split "(\r?\n)") { + if ($FirstLine -and $PSBoundParameters.ContainsKey('FirstLineIndent')) { + $IndentPadding, $FirstLineIndent = $FirstLineIndent, $IndentPadding + } # Don't bother trying to split empty lines if ([String]::IsNullOrWhiteSpace($AnsiRegex.Replace($line, ''))) { Write-Debug "Empty String ($($line.Length))" @@ -64,15 +75,24 @@ filter WrapString { } else { Write-Verbose "Output $($lineLength - $slice.Length)" Write-Verbose "+ $($slice.Length) = $($slice.Length)" - $color = ($color + 1) % $Colors.Length #$null = $output.Append($buffer.ToString()) - $null = $buffer.Append($newline).Append($slice.Text) + $null = $buffer.Append($newline).Append($WrappedIndent).Append($slice.Text) $lineLength = $IndentPadding.Length + $slice.Length } } - $null = $output.Append($buffer.ToString()) - $null = $buffer.Clear().Append($newline).Append($Colors[$color]).Append($IndentPadding) + if (!$FirstLine) { + $null = $output.Append($newline) + } + if ($PSBoundParameters.ContainsKey("IndentPadding")) { + $null = $output.Append($Colors[$color] + $IndentPadding + $buffer.ToString().TrimStart()) + } else { + $null = $output.Append($Colors[$color] + $buffer.ToString()) + } + $color = ($color + 1) % $Colors.Length + $null = $buffer.Clear() #.Append($Colors[$color]).Append($IndentPadding) $lineLength = $IndentPadding.Length + $FirstLine = $false + $IndentPadding = $FirstLineIndent } $output.ToString() } From 85ab35e8c2a147ee569996f702f68631f666412e Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 30 Sep 2024 02:13:56 -0400 Subject: [PATCH 08/15] This is YamlView which is almost the same as Detailed --- source/private/GetYamlRecursive.ps1 | 54 +++++++++++++---------- source/public/ConvertTo-YamlErrorView.ps1 | 18 +++----- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/source/private/GetYamlRecursive.ps1 b/source/private/GetYamlRecursive.ps1 index ac3880e..9862626 100644 --- a/source/private/GetYamlRecursive.ps1 +++ b/source/private/GetYamlRecursive.ps1 @@ -13,7 +13,7 @@ # Optionally, a limit on the depth to recurse properties (defaults to 16) [parameter()] - [int]$depth = 16, + [int]$depth = 1, # If set, include empty and null properties in the output [switch]$IncludeEmpty, @@ -27,18 +27,23 @@ [switch]$XmlAsXml ) process { - $wrap = [Console]::BufferWidth - 1 - ($NestingLevel * 2) + $Width = $Host.UI.RawUI.BufferSize.Width - 1 - ($NestingLevel * 2) + $__hasoutput = $true + $padding = ' ' * $NestingLevel # # lets just create our left-padding for the block + $Recurse = @{ + 'Depth' = $depth + 1 + 'NestingLevel' = $NestingLevel + 1 + 'XmlAsXml' = $XmlAsXml + } + $Wrap = @{ + Width = $Host.UI.RawUI.BufferSize.Width - 2 + IndentPadding = $padding + Colors = $LineColors + } + @( if ($Null -eq $InputObject) { return 'null' } # if it is null return null if ($NestingLevel -eq 0 -and $local:__hasoutput) { '---' } # if we have output before, add a yaml separator - $__hasoutput = $true - $padding = "`n$(' ' * $NestingLevel)" # lets just create our left-padding for the block - $Recurse = @{ - 'Depth' = $depth - 1 - 'NestingLevel' = $NestingLevel + 1 - 'XmlAsXml' = $XmlAsXml - } - $Wrap = try { switch ($InputObject) { @@ -57,7 +62,7 @@ } { $InputObject -is [System.Xml.XmlDocument] -or $InputObject -is [System.Xml.XmlElement] } { "|" - $InputObject.OuterXml | WrapString $Wrap $padding -Colors:$LineColors + $InputObject.OuterXml | WrapString @Wrap break } { $InputObject -is [datetime] -or $InputObject -is [datetimeoffset] } { @@ -77,7 +82,7 @@ } # If we're going to go over our depth, just output like it's a value type # ValueTypes are just output with no possibility of wrapping or recursion - { $InputObject -is [Enum] -or $InputObject.GetType().BaseType -eq [ValueType] -or $depth -eq 1 } { + { $InputObject -is [Enum] -or $InputObject.GetType().BaseType -eq [ValueType] -or $depth -gt $maxDepth } { "$InputObject" break } @@ -87,7 +92,7 @@ { $InputObject -is [System.Collections.IDictionary] } { foreach ($kvp in $InputObject.GetEnumerator()) { # Write-Verbose "$($padding)Enumerate $($property.Name)" - "$padding$accentColor$($kvp.Name):$resetColor " + + "$newline$padding$accentColor$($kvp.Name):$resetColor " + (GetYamlRecursive -InputObject $kvp.Value @Recurse) } break @@ -98,24 +103,27 @@ # Write-Verbose "$($padding)Enumerate $($property.Name)" $Value = GetYamlRecursive -InputObject $item @Recurse # if ($Value -ne 'null' -or $IncludeEmpty) { - "$accentColor$padding$resetColor- $Value" + "$newline$accentColor$padding$resetColor- $Value" # } } break } # Limit recursive enumeration to specific types: - { $InputObject -is [Exception] -or $InputObject -is [System.Management.Automation.ErrorRecord] -or + { $InputObject -is [Exception] -or ($InputObject -is [System.Management.Automation.ErrorRecord] -and $depth -lt 2) -or $InputObject.PSTypeNames[0] -in @( - 'System.Exception' - 'System.Management.Automation.ErrorRecord' + # 'System.Exception' + # 'System.Management.Automation.ErrorRecord' 'Microsoft.Rest.HttpRequestMessageWrapper' 'Microsoft.Rest.HttpResponseMessageWrapper' 'System.Management.Automation.InvocationInfo' ) } { + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } # For exceptions, output a fake property for the exception type if ($InputObject -is [Exception]) { - "$padding${accentColor}#Type:$resetColor ${errorAccentColor}" + $InputObject.GetType().FullName + $resetColor + "$newline$padding${accentColor}#Type:$resetColor ${accentColor}" + $InputObject.GetType().FullName + $resetColor } foreach ($property in $InputObject.PSObject.Properties) { if ($property.Value) { @@ -128,7 +136,7 @@ $Value = "$errorColor$Value$resetColor" } if ((-not [string]::IsNullOrEmpty($Value) -and $Value -ne 'null' -and $Value.Count -gt 0) -or $IncludeEmpty) { - "$padding$accentColor$($property.Name):$resetColor " + $Value + "$newline$padding$accentColor$($property.Name):$resetColor " + $Value } } } @@ -148,9 +156,9 @@ $StringValue = $null if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($InputObject, [string], [ref]$StringValue) -and $null -ne $StringValue) { $StringValue = $StringValue.Trim() - if ($StringValue -match '[\r\n]' -or $StringValue.Length -gt $wrap) { - ">" # signal that we are going to use the readable 'newlines-folded' format - $StringValue | WrapString $Wrap $padding -Colors:$LineColors + if ($StringValue -match '[\r\n]' -or $StringValue.Length -gt $Width) { + ">$newline" # signal that we are going to use the readable 'newlines-folded' format + $StringValue | WrapString @Wrap -EmphasizeOriginalNewlines } elseif ($StringValue.Contains(":")) { "'$($StringValue -replace '''', '''''')'" # single quote it } else { @@ -162,7 +170,7 @@ } } } catch { - Write-Error "Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($InputObject)' Class: $($InputObject.GetType().Name) BaseClass: $($InputObject.GetType().BaseType.Name) " + "Error formatting error ($($_)) in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($InputObject)' Class: $($InputObject.GetType().Name) BaseClass: $($InputObject.GetType().BaseType.Name) " } ) -join "" } diff --git a/source/public/ConvertTo-YamlErrorView.ps1 b/source/public/ConvertTo-YamlErrorView.ps1 index 704bc2d..0e8ce63 100644 --- a/source/public/ConvertTo-YamlErrorView.ps1 +++ b/source/public/ConvertTo-YamlErrorView.ps1 @@ -12,22 +12,14 @@ [System.Management.Automation.ErrorRecord] $InputObject, - # Optionally, a limit on the depth to recurse properties (defaults to 16) - [parameter()] - [int]$depth = 16, + # The maximum depth to recurse into the object + [int]$maxDepth = 10, # If set, include empty and null properties in the output - [switch]$IncludeEmpty, - - # Recursive use only. Handles indentation for formatting - [parameter(DontShow)] - [int]$NestingLevel = 0, - - # use OuterXml instead of treating XmlDocuments like objects - [parameter(DontShow)] - [switch]$XmlAsXml + [switch]$IncludeEmpty ) + begin { ResetColor } process { - GetYamlRecursive $InputObject + GetYamlRecursive -InputObject $InputObject -IncludeEmpty:$IncludeEmpty } } \ No newline at end of file From cfc60de442bafe7239a5078e8d29accbd3a5a422 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 30 Sep 2024 02:14:52 -0400 Subject: [PATCH 09/15] This is ConciseView --- source/private/TruncateString.ps1 | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 source/private/TruncateString.ps1 diff --git a/source/private/TruncateString.ps1 b/source/private/TruncateString.ps1 new file mode 100644 index 0000000..86ffab6 --- /dev/null +++ b/source/private/TruncateString.ps1 @@ -0,0 +1,21 @@ +filter TruncateString { + [CmdletBinding()] + param( + # The input string will be wrapped to a certain length, with optional padding on the front + [Parameter(ValueFromPipeline)] + [string]$InputObject, + + [Parameter(Position = 0)] + [Alias('Length')] + [int]$Width = ($Host.UI.RawUI.BufferSize.Width) + ) + # $wrappableChars = [char[]]" ,.?!:;-`n`r`t" + # $maxLength = $width - $IndentPadding.Length -1 + $wrapper = [Regex]::new("((?:$AnsiPattern)*[^-=,.?!:;\s\r\n\t\\\/\|]+(?:$AnsiPattern)*)", "Compiled") + + if ($InputObject.Length -le $Width) { + return $InputObject + } + + ($InputObject.Substring(0,$length) -split $wrapper,-2)[0] +} \ No newline at end of file From 47c2a4809e90cd13209df173325a18cd479da908 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 30 Sep 2024 02:15:11 -0400 Subject: [PATCH 10/15] This is DetailedView --- source/private/GetListRecursive.ps1 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/source/private/GetListRecursive.ps1 b/source/private/GetListRecursive.ps1 index 0b3e4d2..ef943ea 100644 --- a/source/private/GetListRecursive.ps1 +++ b/source/private/GetListRecursive.ps1 @@ -78,7 +78,8 @@ function GetListRecursive { $Wrap = @{ Width = $Host.UI.RawUI.BufferSize.Width - 2 IndentPadding = "" - HangingIndent = " " + WrappedIndent = " " + Colors = $LineColors } $null = $output.Append(($prop.Value | WrapString @Wrap)) } elseif ($prop.Name -eq 'HResult') { @@ -111,9 +112,9 @@ function GetListRecursive { if ($value -is [Type]) { # Just show the typename instead of it as an object - $null = $output.Append("${nextPadding}[$($value.ToString())]") + $null = $output.Append("[$($value.ToString())]") } elseif ($value -is [string] -or $value.GetType().IsPrimitive) { - $null = $output.Append("${nextPadding}${value}") + $null = $output.Append("${value}") } else { if (!$isFirstElement) { $null = $output.Append($newline) @@ -141,10 +142,14 @@ function GetListRecursive { } $Wrap = @{ Width = $Host.UI.RawUI.BufferSize.Width - 2 + # Because the first line contains the property name, we don't want to indent it + FirstLineIndent = '' + # But all other lines (including wrapped lines) should be indented to align IndentPadding = " " * ($nextIndent + $prop.Name.Length) + Colors = $LineColors } - $null = $output.Append(($value | WrapString @Wrap).TrimStart()) + $null = $output.Append(($value | WrapString @Wrap)) } } From e103abf69c2ed355853ec239896320bd86eab41f Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 7 Oct 2024 01:36:17 -0400 Subject: [PATCH 11/15] Detect Github Workflow and Azure Pipelines and use DetailedView --- build.psd1 | 1 + source/postfix.ps1 | 7 +++++++ source/prefix.ps1 | 22 +++------------------- source/public/Set-ErrorView.ps1 | 3 ++- 4 files changed, 13 insertions(+), 20 deletions(-) create mode 100644 source/postfix.ps1 diff --git a/build.psd1 b/build.psd1 index a3d17d5..bc39a65 100644 --- a/build.psd1 +++ b/build.psd1 @@ -2,6 +2,7 @@ ModuleManifest = "./source/ErrorView.psd1" CopyPaths = 'ErrorView.format.ps1xml' Prefix = 'prefix.ps1' + Postfix = 'postfix.ps1' # The rest of the paths are relative to the manifest OutputDirectory = ".." VersionedOutputDirectory = $true diff --git a/source/postfix.ps1 b/source/postfix.ps1 new file mode 100644 index 0000000..bac8866 --- /dev/null +++ b/source/postfix.ps1 @@ -0,0 +1,7 @@ +if ($script:ErrorView) { + Set-ErrorView $ErrorView +} elseif ($Env:GITHUB_ACTIONS -or $Env:TF_BUILD) { + Set-ErrorView "DetailedErrorView" +} else { + Set-ErrorView "ConciseView" +} \ No newline at end of file diff --git a/source/prefix.ps1 b/source/prefix.ps1 index 5ab3ea2..a116ae8 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1,33 +1,17 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'ErrorView is all about the ErrorView global variable')] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Seriously. Stop complaining about ErrorView')] param( - $global:ErrorView = "Simple" + $ErrorView ) -# We need to _overwrite_ the ErrorView -# So -PrependPath, instead of using FormatsToProcess +# We need to _overwrite_ the ErrorView, so we must use -PrependPath Update-FormatData -PrependPath $PSScriptRoot\ErrorView.format.ps1xml Set-StrictMode -Off $ErrorActionPreference = 'Stop' trap { 'Error found in error view definition: ' + $_.Exception.Message } -# Borrowed this one from https://github.com/chalk/ansi-regex -$script:AnsiEscapes = [Regex]::new("([\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?(?:\u001b\u005c|\u0007))|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])))", "Compiled"); - -# starting with an escape character and then... -# ESC ] <anything> <ST> - where ST is either 1B 5C or 7 (BEL, aka `a) -# ESC [ non-letters letter (or ~, =, @, >) -# ESC ( <any character> -# ESC O P -# ESC O Q -# ESC O R -# ESC O S -# $script:AnsiEscapes = [Regex]::new("\x1b[\(\)%`"&\.\/*+.-][@-Z]|\x1b\].*?(?:\u001b\u005c|\u0007|^)|\x1b\[\P{L}*[@-_A-Za-z^`\{\|\}~]|\x1b#\d|\x1b[!-~]", [System.Text.RegularExpressions.RegexOptions]::Compiled); - $script:ellipsis = [char]0x2026 $script:newline = [Environment]::Newline $script:LineColors = @( "`e[38;2;255;255;255m" "`e[38;2;179;179;179m" -) +) \ No newline at end of file diff --git a/source/public/Set-ErrorView.ps1 b/source/public/Set-ErrorView.ps1 index 340394a..7a0d5e6 100644 --- a/source/public/Set-ErrorView.ps1 +++ b/source/public/Set-ErrorView.ps1 @@ -21,7 +21,8 @@ filter Set-ErrorView { })] $View = "Normal" ) - # Update the enum every time, because how often do you change the error view? + + # Re-create an update the enum every time, because how often do you change the error view? $Names = [System.Management.Automation.ErrorView].GetEnumNames() + @( Get-Command ConvertTo-*ErrorView -ListImported -ParameterName InputObject -ParameterType [System.Management.Automation.ErrorRecord] ).Name -replace "ConvertTo-(\w+)ErrorView", '$1View' | Select-Object -Unique From 76e474ad222b65a8763d75f1b2e55decd68d4b67 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 7 Oct 2024 01:38:43 -0400 Subject: [PATCH 12/15] Add explicit formatting for Github Actions and Azure Pipelines Closes #2 and #3 --- .../GetAzurePipelinePositionMessage.ps1 | 36 +++++++++++++++++ ...itionMessage.ps1 => GetConciseMessage.ps1} | 2 +- source/private/GetErrorMessage.ps1 | 23 +++++++++++ source/private/GetErrorTitle.ps1 | 29 ++++++++++++++ .../GetGoogleWorkflowPositionMessage.ps1 | 39 +++++++++++++++++++ source/private/WrapString.ps1 | 10 ----- source/public/ConvertTo-ConciseErrorView.ps1 | 2 +- source/public/ConvertTo-DetailedErrorView.ps1 | 7 +++- 8 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 source/private/GetAzurePipelinePositionMessage.ps1 rename source/private/{GetConciseViewPositionMessage.ps1 => GetConciseMessage.ps1} (99%) create mode 100644 source/private/GetErrorMessage.ps1 create mode 100644 source/private/GetErrorTitle.ps1 create mode 100644 source/private/GetGoogleWorkflowPositionMessage.ps1 diff --git a/source/private/GetAzurePipelinePositionMessage.ps1 b/source/private/GetAzurePipelinePositionMessage.ps1 new file mode 100644 index 0000000..a32031e --- /dev/null +++ b/source/private/GetAzurePipelinePositionMessage.ps1 @@ -0,0 +1,36 @@ +filter GetGooglePositionMessage { + [CmdletBinding()] + [OUtputType([string])] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + $InvocationInfo = $InputObject.InvocationInfo + # Handle case where there is a TargetObject from a Pester `Should` assertion failure and we can show the error at the target rather than the script source + # Note that in some versions, this is a Dictionary<,> and in others it's a hashtable. So we explicitly cast to a shared interface in the method invocation + # to force using `IDictionary.Contains`. Hashtable does have it's own `ContainKeys` as well, but if they ever opt to use a custom `IDictionary`, that may not. + $useTargetObject = $null -ne $InputObject.TargetObject -and + $InputObject.TargetObject -is [System.Collections.IDictionary] -and + ([System.Collections.IDictionary]$InputObject.TargetObject).Contains('Line') -and + ([System.Collections.IDictionary]$InputObject.TargetObject).Contains('LineText') + + $file = if ($useTargetObject) { + "$($InputObject.TargetObject.File)" + } elseif (.ScriptName) { + "$($InvocationInfo.ScriptName)" + } + + $line = if ($useTargetObject) { + $InputObject.TargetObject.Line + } else { + $InvocationInfo.ScriptLineNumber + } + + if ($useTargetObject) { + "sourcepath=$file;linenumber=$line" + } else { + $column = $InvocationInfo.OffsetInLine + "sourcepath=$file;linenumber=$line,columnnumber=$column" + } +} diff --git a/source/private/GetConciseViewPositionMessage.ps1 b/source/private/GetConciseMessage.ps1 similarity index 99% rename from source/private/GetConciseViewPositionMessage.ps1 rename to source/private/GetConciseMessage.ps1 index 7acdaac..a9ac955 100644 --- a/source/private/GetConciseViewPositionMessage.ps1 +++ b/source/private/GetConciseMessage.ps1 @@ -1,4 +1,4 @@ -filter GetConciseViewPositionMessage { +filter GetConciseMessage { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] diff --git a/source/private/GetErrorMessage.ps1 b/source/private/GetErrorMessage.ps1 new file mode 100644 index 0000000..e7d77cc --- /dev/null +++ b/source/private/GetErrorMessage.ps1 @@ -0,0 +1,23 @@ +filter GetErrorTitle { + [CmdletBinding()] + [OUtputType([string])] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + if (! $err.ErrorDetails -or ! $err.ErrorDetails.Message) { + if ($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception.Message.Contains("~$newline")) { + # need to parse out the relevant part of the pre-rendered positionmessage + $err.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } elseif ($err.Exception) { + $err.Exception.Message + } elseif ($err.Message) { + $err.Message + } else { + $err.ToString() + } + } else { + $err.ErrorDetails.Message + } +} \ No newline at end of file diff --git a/source/private/GetErrorTitle.ps1 b/source/private/GetErrorTitle.ps1 new file mode 100644 index 0000000..f1f779f --- /dev/null +++ b/source/private/GetErrorTitle.ps1 @@ -0,0 +1,29 @@ +filter GetErrorTitle { + [CmdletBinding()] + [OUtputType([string])] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + + if ($InputObject.Exception -and $InputObject.Exception.WasThrownFromThrowStatement) { + 'Exception' + # MyCommand can be the script block, so we don't want to show that so check if it's an actual command + } elseif ($InputObject.InvocationInfo.MyCommand -and $InputObject.InvocationInfo.MyCommand.Name -and (Get-Command -Name $InputObject.InvocationInfo.MyCommand -ErrorAction Ignore)) { + $InputObject.InvocationInfo.MyCommand + } elseif ($InputObject.CategoryInfo.Activity) { + # If it's a scriptblock, better to show the command in the scriptblock that had the error + $InputObject.CategoryInfo.Activity + } elseif ($InputObject.InvocationInfo.MyCommand) { + $InputObject.InvocationInfo.MyCommand + } elseif ($InputObject.InvocationInfo.InvocationName) { + $InputObject.InvocationInfo.InvocationName + } elseif ($InputObject.CategoryInfo.Category) { + $InputObject.CategoryInfo.Category + } elseif ($InputObject.CategoryInfo.Reason) { + $InputObject.CategoryInfo.Reason + } else { + 'Error' + } +} \ No newline at end of file diff --git a/source/private/GetGoogleWorkflowPositionMessage.ps1 b/source/private/GetGoogleWorkflowPositionMessage.ps1 new file mode 100644 index 0000000..6faac67 --- /dev/null +++ b/source/private/GetGoogleWorkflowPositionMessage.ps1 @@ -0,0 +1,39 @@ +filter GetGoogleWorkflowPositionMessage { + [CmdletBinding()] + [OUtputType([string])] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + $InvocationInfo = $InputObject.InvocationInfo + # Handle case where there is a TargetObject from a Pester `Should` assertion failure and we can show the error at the target rather than the script source + # Note that in some versions, this is a Dictionary<,> and in others it's a hashtable. So we explicitly cast to a shared interface in the method invocation + # to force using `IDictionary.Contains`. Hashtable does have it's own `ContainKeys` as well, but if they ever opt to use a custom `IDictionary`, that may not. + $useTargetObject = $null -ne $InputObject.TargetObject -and + $InputObject.TargetObject -is [System.Collections.IDictionary] -and + ([System.Collections.IDictionary]$InputObject.TargetObject).Contains('Line') -and + ([System.Collections.IDictionary]$InputObject.TargetObject).Contains('LineText') + + $file = if ($useTargetObject) { + "$($InputObject.TargetObject.File)" + } elseif (.ScriptName) { + "$($InvocationInfo.ScriptName)" + } + + $line = if ($useTargetObject) { + $InputObject.TargetObject.Line + } else { + $InvocationInfo.ScriptLineNumber + } + + if ($useTargetObject) { + "file=$file,line=$line" + } else { + $column = $InvocationInfo.OffsetInLine + + $Length = $InvocationInfo.PositionMessage.Split($newline)[-1].Substring(1).Trim().Length + $endColumn = $column + $Length + "file=$file,line=$line,col=$column,endColumn=$endColumn" + } +} diff --git a/source/private/WrapString.ps1 b/source/private/WrapString.ps1 index ed52337..e825dd1 100644 --- a/source/private/WrapString.ps1 +++ b/source/private/WrapString.ps1 @@ -1,13 +1,3 @@ -$script:AnsiPattern = "[\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?(?:\u001b\u005c|\u0007))|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))" -$script:AnsiRegex = [Regex]::new($AnsiPattern, "Compiled"); -function MeasureString { - [CmdletBinding()] - param( - [string]$InputObject - ) - $AnsiRegex.Replace($InputObject, '').Length -} - filter WrapString { [CmdletBinding()] diff --git a/source/public/ConvertTo-ConciseErrorView.ps1 b/source/public/ConvertTo-ConciseErrorView.ps1 index f36b53b..390e6b4 100644 --- a/source/public/ConvertTo-ConciseErrorView.ps1 +++ b/source/public/ConvertTo-ConciseErrorView.ps1 @@ -15,7 +15,7 @@ function ConvertTo-ConciseErrorView { $local:resetColor = "<<<" } - $message = GetConciseViewPositionMessage -InputObject $InputObject + $message = GetConciseMessage -InputObject $InputObject if ($InputObject.PSMessageDetails) { $message = $errorColor + ' : ' + $InputObject.PSMessageDetails + $message diff --git a/source/public/ConvertTo-DetailedErrorView.ps1 b/source/public/ConvertTo-DetailedErrorView.ps1 index 555ba16..437a1ba 100644 --- a/source/public/ConvertTo-DetailedErrorView.ps1 +++ b/source/public/ConvertTo-DetailedErrorView.ps1 @@ -22,5 +22,10 @@ filter ConvertTo-DetailedErrorView { begin { ResetColor } process { $newline + (GetListRecursive $InputObject) + $newline + if ($Env:GITHUB_ACTIONS) { + Write-Host "::error $(GetGoogleWorkflowPositionMesage),title=$(GetErrorTitle $InputObject)::$(GetErrorMessage $InputObject)" + } elseif ($Env:TF_BUILD) { + Write-Host "##vso[task.logissue type=error;$(GetAzurePipelinesPositionMesage)]$(GetErrorTitle $InputObject): $(GetErrorMessage $InputObject)" + } } -} +} \ No newline at end of file From c2e893719c9b3fce20ca4c87ab65be3d0eb13a0e Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 7 Oct 2024 01:53:14 -0400 Subject: [PATCH 13/15] Clean up PSScriptAnalyzer --- Reference/default.ps1 | 2 +- ScriptAnalyzerSettings.psd1 | 4 ++++ source/private/GetConciseMessage.ps1 | 4 +--- source/private/GetErrorMessage.ps1 | 20 +++++++++---------- source/public/ConvertTo-DetailedErrorView.ps1 | 4 ++-- source/public/ConvertTo-YamlErrorView.ps1 | 1 + 6 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 ScriptAnalyzerSettings.psd1 diff --git a/Reference/default.ps1 b/Reference/default.ps1 index 6108b80..cae60b6 100644 --- a/Reference/default.ps1 +++ b/Reference/default.ps1 @@ -150,7 +150,7 @@ function Get-ConciseViewPositionMessage { $message = $message.Replace($newline, ' ').Replace("`n", ' ').Replace("`t", ' ') $windowWidth = 120 - if ($Host.UI.RawUI -ne $null) { + if ($null -ne $Host.UI.RawUI) { $windowWidth = $Host.UI.RawUI.WindowSize.Width } diff --git a/ScriptAnalyzerSettings.psd1 b/ScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..caca8da --- /dev/null +++ b/ScriptAnalyzerSettings.psd1 @@ -0,0 +1,4 @@ +@{ + Severity = @('Error', 'Warning') + ExcludeRules = @('PSAvoidGlobalVars') +} diff --git a/source/private/GetConciseMessage.ps1 b/source/private/GetConciseMessage.ps1 index a9ac955..707194f 100644 --- a/source/private/GetConciseMessage.ps1 +++ b/source/private/GetConciseMessage.ps1 @@ -8,7 +8,6 @@ filter GetConciseMessage { $err = $InputObject $posmsg = '' $headerWhitespace = '' - $offsetWhitespace = '' $message = '' $prefix = '' @@ -90,7 +89,6 @@ filter GetConciseMessage { } $posmsg += "${accentColor}${lineWhitespace}${ScriptLineNumber} ${verticalBar} ${resetcolor}${line}" - $offsetWhitespace = ' ' * $offsetInLine $prefix = "${accentColor}${headerWhitespace} ${verticalBar} ${errorColor}" if ($highlightLine -ne '') { $posMsg += "${prefix}${highlightLine}${newline}" @@ -122,7 +120,7 @@ filter GetConciseMessage { $message = $message.Replace($newline, ' ').Replace("`n", ' ').Replace("`t", ' ') $windowWidth = 120 - if ($Host.UI.RawUI -ne $null) { + if ($null -ne $Host.UI.RawUI) { $windowWidth = $Host.UI.RawUI.WindowSize.Width } diff --git a/source/private/GetErrorMessage.ps1 b/source/private/GetErrorMessage.ps1 index e7d77cc..515abfe 100644 --- a/source/private/GetErrorMessage.ps1 +++ b/source/private/GetErrorMessage.ps1 @@ -6,18 +6,18 @@ filter GetErrorTitle { [System.Management.Automation.ErrorRecord] $InputObject ) - if (! $err.ErrorDetails -or ! $err.ErrorDetails.Message) { - if ($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception.Message.Contains("~$newline")) { + if ($InputObject.ErrorDetails -and $InputObject.ErrorDetails.Message) { + $InputObject.ErrorDetails.Message + } else { + if ($InputObject.CategoryInfo.Category -eq 'ParserError' -and $InputObject.Exception.Message.Contains("~$newline")) { # need to parse out the relevant part of the pre-rendered positionmessage - $err.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] - } elseif ($err.Exception) { - $err.Exception.Message - } elseif ($err.Message) { - $err.Message + $InputObject.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } elseif ($InputObject.Exception) { + $InputObject.Exception.Message + } elseif ($InputObject.Message) { + $InputObject.Message } else { - $err.ToString() + $InputObject.ToString() } - } else { - $err.ErrorDetails.Message } } \ No newline at end of file diff --git a/source/public/ConvertTo-DetailedErrorView.ps1 b/source/public/ConvertTo-DetailedErrorView.ps1 index 437a1ba..539b43f 100644 --- a/source/public/ConvertTo-DetailedErrorView.ps1 +++ b/source/public/ConvertTo-DetailedErrorView.ps1 @@ -23,9 +23,9 @@ filter ConvertTo-DetailedErrorView { process { $newline + (GetListRecursive $InputObject) + $newline if ($Env:GITHUB_ACTIONS) { - Write-Host "::error $(GetGoogleWorkflowPositionMesage),title=$(GetErrorTitle $InputObject)::$(GetErrorMessage $InputObject)" + "::error $(GetGoogleWorkflowPositionMesage),title=$(GetErrorTitle $InputObject)::$(GetErrorMessage $InputObject)" } elseif ($Env:TF_BUILD) { - Write-Host "##vso[task.logissue type=error;$(GetAzurePipelinesPositionMesage)]$(GetErrorTitle $InputObject): $(GetErrorMessage $InputObject)" + "##vso[task.logissue type=error;$(GetAzurePipelinesPositionMesage)]$(GetErrorTitle $InputObject): $(GetErrorMessage $InputObject)" } } } \ No newline at end of file diff --git a/source/public/ConvertTo-YamlErrorView.ps1 b/source/public/ConvertTo-YamlErrorView.ps1 index 0e8ce63..089e314 100644 --- a/source/public/ConvertTo-YamlErrorView.ps1 +++ b/source/public/ConvertTo-YamlErrorView.ps1 @@ -5,6 +5,7 @@ .DESCRIPTION This produces valid Yaml output from ErrorRecord you pass to it, recursively. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'maxDepth')] [CmdletBinding()] param( # The object that you want to convert to YAML From 1a56d18984b86ee3a76d8f014b40bf013e27085f Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 7 Oct 2024 01:53:59 -0400 Subject: [PATCH 14/15] Additional reference files, just for the record. --- Reference/FileInfo.format.ps1xml | 206 ++++++ Reference/LegacyErrorView.format.ps1xml | 1 + Reference/OriginalErrorView.format.ps1xml | 600 ++++++++++++++++++ Reference/OriginalExceptionView.format.ps1xml | 45 ++ 4 files changed, 852 insertions(+) create mode 100644 Reference/FileInfo.format.ps1xml create mode 100644 Reference/LegacyErrorView.format.ps1xml create mode 100644 Reference/OriginalErrorView.format.ps1xml create mode 100644 Reference/OriginalExceptionView.format.ps1xml diff --git a/Reference/FileInfo.format.ps1xml b/Reference/FileInfo.format.ps1xml new file mode 100644 index 0000000..745a045 --- /dev/null +++ b/Reference/FileInfo.format.ps1xml @@ -0,0 +1,206 @@ +<?xml version="1.0" encoding="utf-8"?> +<Configuration> + <ViewDefinitions> + <View> + <Name>children</Name> + <ViewSelectedBy> + <TypeName>System.IO.DirectoryInfo</TypeName> + </ViewSelectedBy> + <GroupBy> + <PropertyName>PSParentPath</PropertyName> + </GroupBy> + <TableControl> + <TableHeaders> + <TableColumnHeader> + <Label>Mode</Label> + <Width>7</Width> + <Alignment>Left</Alignment> + </TableColumnHeader> + <TableColumnHeader> + <Label>LastWriteTime</Label> + <Width>26</Width> + <Alignment>Right</Alignment> + </TableColumnHeader> + <TableColumnHeader> + <Label>Length</Label> + <Width>14</Width> + <Alignment>Right</Alignment> + </TableColumnHeader> + <TableColumnHeader> + <Label>Name</Label> + <Alignment>Left</Alignment> + </TableColumnHeader> + </TableHeaders> + <TableRowEntries> + <TableRowEntry> + <Wrap /> + <TableColumnItems> + <TableColumnItem> + <PropertyName>ModeWithoutHardLink</PropertyName> + </TableColumnItem> + <TableColumnItem> + <PropertyName>LastWriteTimeString</PropertyName> + </TableColumnItem> + <TableColumnItem> + <PropertyName>LengthString</PropertyName> + </TableColumnItem> + <TableColumnItem> + <PropertyName>NameString</PropertyName> + </TableColumnItem> + </TableColumnItems> + </TableRowEntry> + </TableRowEntries> + </TableControl> + </View> + <View> + <Name>childrenWithHardlink</Name> + <ViewSelectedBy> + <TypeName>System.IO.DirectoryInfo</TypeName> + </ViewSelectedBy> + <GroupBy> + <PropertyName>PSParentPath</PropertyName> + </GroupBy> + <TableControl> + <TableHeaders> + <TableColumnHeader> + <Label>Mode</Label> + <Width>7</Width> + <Alignment>Left</Alignment> + </TableColumnHeader> + <TableColumnHeader> + <Label>LastWriteTime</Label> + <Width>26</Width> + <Alignment>Right</Alignment> + </TableColumnHeader> + <TableColumnHeader> + <Label>Length</Label> + <Width>14</Width> + <Alignment>Right</Alignment> + </TableColumnHeader> + <TableColumnHeader> + <Label>Name</Label> + <Alignment>Left</Alignment> + </TableColumnHeader> + </TableHeaders> + <TableRowEntries> + <TableRowEntry> + <Wrap /> + <TableColumnItems> + <TableColumnItem> + <PropertyName>Mode</PropertyName> + </TableColumnItem> + <TableColumnItem> + <PropertyName>LastWriteTimeString</PropertyName> + </TableColumnItem> + <TableColumnItem> + <PropertyName>LengthString</PropertyName> + </TableColumnItem> + <TableColumnItem> + <PropertyName>NameString</PropertyName> + </TableColumnItem> + </TableColumnItems> + </TableRowEntry> + </TableRowEntries> + </TableControl> + </View> + <View> + <Name>children</Name> + <ViewSelectedBy> + <TypeName>System.IO.DirectoryInfo</TypeName> + </ViewSelectedBy> + <GroupBy> + <PropertyName>PSParentPath</PropertyName> + </GroupBy> + <ListControl> + <ListEntries> + <ListEntry> + <ListItems> + <ListItem> + <PropertyName>Name</PropertyName> + </ListItem> + <ListItem> + <PropertyName>CreationTime</PropertyName> + </ListItem> + <ListItem> + <PropertyName>LastWriteTime</PropertyName> + </ListItem> + <ListItem> + <PropertyName>LastAccessTime</PropertyName> + </ListItem> + <ListItem> + <PropertyName>Mode</PropertyName> + </ListItem> + <ListItem> + <PropertyName>LinkType</PropertyName> + </ListItem> + <ListItem> + <PropertyName>Target</PropertyName> + </ListItem> + </ListItems> + </ListEntry> + <ListEntry> + <EntrySelectedBy> + <TypeName>System.IO.FileInfo</TypeName> + </EntrySelectedBy> + <ListItems> + <ListItem> + <PropertyName>Name</PropertyName> + </ListItem> + <ListItem> + <Label>Length</Label> + <PropertyName>LengthString</PropertyName> + </ListItem> + <ListItem> + <PropertyName>CreationTime</PropertyName> + </ListItem> + <ListItem> + <PropertyName>LastWriteTime</PropertyName> + </ListItem> + <ListItem> + <PropertyName>LastAccessTime</PropertyName> + </ListItem> + <ListItem> + <PropertyName>Mode</PropertyName> + </ListItem> + <ListItem> + <PropertyName>LinkType</PropertyName> + </ListItem> + <ListItem> + <PropertyName>Target</PropertyName> + </ListItem> + <ListItem> + <PropertyName>VersionInfo</PropertyName> + </ListItem> + </ListItems> + </ListEntry> + </ListEntries> + </ListControl> + </View> + <View> + <Name>children</Name> + <ViewSelectedBy> + <TypeName>System.IO.DirectoryInfo</TypeName> + </ViewSelectedBy> + <GroupBy> + <PropertyName>PSParentPath</PropertyName> + </GroupBy> + <WideControl> + <WideEntries> + <WideEntry> + <WideItem> + <PropertyName>Name</PropertyName> + </WideItem> + </WideEntry> + <WideEntry> + <EntrySelectedBy> + <TypeName>System.IO.DirectoryInfo</TypeName> + </EntrySelectedBy> + <WideItem> + <PropertyName>Name</PropertyName> + </WideItem> + </WideEntry> + </WideEntries> + </WideControl> + </View> + </ViewDefinitions> +</Configuration> \ No newline at end of file diff --git a/Reference/LegacyErrorView.format.ps1xml b/Reference/LegacyErrorView.format.ps1xml new file mode 100644 index 0000000..fde1bd3 --- /dev/null +++ b/Reference/LegacyErrorView.format.ps1xml @@ -0,0 +1 @@ +<?xml version="1.0" encoding="utf-8"?><Configuration><ViewDefinitions /></Configuration> \ No newline at end of file diff --git a/Reference/OriginalErrorView.format.ps1xml b/Reference/OriginalErrorView.format.ps1xml new file mode 100644 index 0000000..835a083 --- /dev/null +++ b/Reference/OriginalErrorView.format.ps1xml @@ -0,0 +1,600 @@ +<?xml version="1.0" encoding="utf-8"?> +<Configuration> + <ViewDefinitions> + <View> + <Name>GetErrorInstance</Name> + <ViewSelectedBy> + <TypeName>System.Management.Automation.ErrorRecord#PSExtendedError</TypeName> + </ViewSelectedBy> + <GroupBy> + <PropertyName>PSErrorIndex</PropertyName> + <Label>ErrorIndex</Label> + </GroupBy> + <CustomControl> + <CustomEntries> + <CustomEntry> + <CustomItem> + <ExpressionBinding> + <ScriptBlock> + Set-StrictMode -Off + + $maxDepth = 10 + $ellipsis = "`u{2026}" + $resetColor = '' + $errorColor = '' + $accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = $PSStyle.Reset + $errorColor = $psstyle.Formatting.Error + $accentColor = $PSStyle.Formatting.FormatAccent + } + + function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) { + $newline = [Environment]::Newline + $output = [System.Text.StringBuilder]::new() + $prefix = ' ' * $indent + + $expandTypes = @( + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) + + # if object is an Exception, add an ExceptionType property + if ($obj -is [Exception]) { + $obj | Add-Member -NotePropertyName Type -NotePropertyValue $obj.GetType().FullName -ErrorAction Ignore + } + + # first find the longest property so we can indent properly + $propLength = 0 + foreach ($prop in $obj.PSObject.Properties) { + if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) { + $propLength = $prop.Name.Length + } + } + + $addedProperty = $false + foreach ($prop in $obj.PSObject.Properties) { + + # don't show empty properties or our added property for $error[index] + if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') { + $addedProperty = $true + $null = $output.Append($prefix) + $null = $output.Append($accentColor) + $null = $output.Append($prop.Name) + $propNameIndent = ' ' * ($propLength - $prop.Name.Length) + $null = $output.Append($propNameIndent) + $null = $output.Append(' : ') + $null = $output.Append($resetColor) + + $newIndent = $indent + 4 + + # only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes + if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or + $expandTypes -contains $prop.TypeNameOfValue -or ($prop.TypeNames -ne $null -and $expandTypes -contains $prop.TypeNames[0])) { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $null = $output.Append($newline) + $null = $output.Append((Show-ErrorRecord $prop.Value $newIndent ($depth + 1))) + } + } + # `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members + elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') { + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $targetSite = [PSCustomObject]@{ + Name = $prop.Value.Name + DeclaringType = $prop.Value.DeclaringType + MemberType = $prop.Value.MemberType + Module = $prop.Value.Module + } + + $null = $output.Append($newline) + $null = $output.Append((Show-ErrorRecord $targetSite $newIndent ($depth + 1))) + } + } + # `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation + elseif ($prop.Name -eq 'StackTrace') { + # for a stacktrace which is usually quite wide with info, we left justify it + $null = $output.Append($newline) + $null = $output.Append($prop.Value) + } + # Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here + elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') { + $isFirstElement = $true + foreach ($key in $prop.Value.Keys) { + if ($isFirstElement) { + $null = $output.Append($newline) + } + + if ($key -eq 'Authorization') { + $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}${ellipsis}${newline}") + } + else { + $null = $output.Append("${prefix} ${accentColor}${key} : ${resetColor}$($prop.Value[$key])${newline}") + } + + $isFirstElement = $false + } + } + # if the object implements IEnumerable and not a string, we try to show each object + # We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here + elseif (!($prop.Value -is [System.String]) -and $prop.Value.GetType().GetInterface('IEnumerable') -ne $null -and $prop.Name -ne 'Data') { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } + else { + $isFirstElement = $true + foreach ($value in $prop.Value) { + $null = $output.Append($newline) + $valueIndent = ' ' * ($newIndent + 2) + + if ($value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("${prefix}${valueIndent}[$($value.ToString())]") + } + elseif ($value -is [string] -or $value.GetType().IsPrimitive) { + $null = $output.Append("${prefix}${valueIndent}${value}") + } + else { + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1))) + } + $isFirstElement = $false + } + } + } + elseif ($prop.Value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("[$($prop.Value.ToString())]") + } + # Anything else, we convert to string. + # ToString() can throw so we use LanguagePrimitives.TryConvertTo() to hide a convert error + else { + $value = $null + if ([System.Management.Automation.LanguagePrimitives]::TryConvertTo($prop.Value, [string], [ref]$value) -and $value -ne $null) + { + if ($prop.Name -eq 'PositionMessage') { + $value = $value.Insert($value.IndexOf('~'), $errorColor) + } + elseif ($prop.Name -eq 'Message') { + $value = $errorColor + $value + } + + $isFirstLine = $true + if ($value.Contains($newline)) { + # the 3 is to account for ' : ' + $valueIndent = ' ' * ($propLength + 3) + # need to trim any extra whitespace already in the text + foreach ($line in $value.Split($newline)) { + if (!$isFirstLine) { + $null = $output.Append("${newline}${prefix}${valueIndent}") + } + $null = $output.Append($line.Trim()) + $isFirstLine = $false + } + } + else { + $null = $output.Append($value) + } + } + } + + $null = $output.Append($newline) + } + } + + # if we had added nested properties, we need to remove the last newline + if ($addedProperty) { + $null = $output.Remove($output.Length - $newline.Length, $newline.Length) + } + + $output.ToString() + } + + # Add back original typename and remove PSExtendedError + if ($_.PSObject.TypeNames.Contains('System.Management.Automation.ErrorRecord#PSExtendedError')) { + $_.PSObject.TypeNames.Add('System.Management.Automation.ErrorRecord') + $null = $_.PSObject.TypeNames.Remove('System.Management.Automation.ErrorRecord#PSExtendedError') + } + elseif ($_.PSObject.TypeNames.Contains('System.Exception#PSExtendedError')) { + $_.PSObject.TypeNames.Add('System.Exception') + $null = $_.PSObject.TypeNames.Remove('System.Exception#PSExtendedError') + } + + Show-ErrorRecord $_ + </ScriptBlock> + </ExpressionBinding> + </CustomItem> + </CustomEntry> + </CustomEntries> + </CustomControl> + </View> + <View> + <Name>ErrorInstance</Name> + <ViewSelectedBy> + <TypeName>System.Management.Automation.ErrorRecord</TypeName> + </ViewSelectedBy> + <OutOfBand /> + <CustomControl> + <CustomEntries> + <CustomEntry> + <CustomItem> + <ExpressionBinding> + <ScriptBlock> $errorColor = '' + $commandPrefix = '' + if (@('NativeCommandErrorMessage','NativeCommandError') -notcontains $_.FullyQualifiedErrorId -and @('CategoryView','ConciseView','DetailedView') -notcontains $ErrorView) + { + $myinv = $_.InvocationInfo + if ($Host.UI.SupportsVirtualTerminal) { + $errorColor = $PSStyle.Formatting.Error + } + + $commandPrefix = if ($myinv -and $myinv.MyCommand) { + switch -regex ( $myinv.MyCommand.CommandType ) + { + ([System.Management.Automation.CommandTypes]::ExternalScript) + { + if ($myinv.MyCommand.Path) + { + $myinv.MyCommand.Path + ' : ' + } + + break + } + + ([System.Management.Automation.CommandTypes]::Script) + { + if ($myinv.MyCommand.ScriptBlock) + { + $myinv.MyCommand.ScriptBlock.ToString() + ' : ' + } + + break + } + default + { + if ($myinv.InvocationName -match '^[&\.]?$') + { + if ($myinv.MyCommand.Name) + { + $myinv.MyCommand.Name + ' : ' + } + } + else + { + $myinv.InvocationName + ' : ' + } + + break + } + } + } + elseif ($myinv -and $myinv.InvocationName) + { + $myinv.InvocationName + ' : ' + } + } + + $errorColor + $commandPrefix</ScriptBlock> + </ExpressionBinding> + <ExpressionBinding> + <ScriptBlock> Set-StrictMode -Off + $ErrorActionPreference = 'Stop' + trap { 'Error found in error view definition: ' + $_.Exception.Message } + $newline = [Environment]::Newline + + $resetColor = '' + $errorColor = '' + $accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = $PSStyle.Reset + $errorColor = $PSStyle.Formatting.Error + $accentColor = $PSStyle.Formatting.ErrorAccent + } + + function Get-ConciseViewPositionMessage { + + # returns a string cut to last whitespace + function Get-TruncatedString($string, [int]$length) { + + if ($string.Length -le $length) { + return $string + } + + return ($string.Substring(0,$length) -split '\s',-2)[0] + } + + $posmsg = '' + $headerWhitespace = '' + $offsetWhitespace = '' + $message = '' + $prefix = '' + + # Handle case where there is a TargetObject from a Pester `Should` assertion failure and we can show the error at the target rather than the script source + # Note that in some versions, this is a Dictionary<,> and in others it's a hashtable. So we explicitly cast to a shared interface in the method invocation + # to force using `IDictionary.Contains`. Hashtable does have it's own `ContainKeys` as well, but if they ever opt to use a custom `IDictionary`, that may not. + $useTargetObject = $null -ne $err.TargetObject -and + $err.TargetObject -is [System.Collections.IDictionary] -and + ([System.Collections.IDictionary]$err.TargetObject).Contains('Line') -and + ([System.Collections.IDictionary]$err.TargetObject).Contains('LineText') + + # The checks here determine if we show line detailed error information: + # - check if `ParserError` and comes from PowerShell which eventually results in a ParseException, but during this execution it's an ErrorRecord + $isParseError = $err.CategoryInfo.Category -eq 'ParserError' -and + $err.Exception -is [System.Management.Automation.ParentContainsErrorRecordException] + + # - check if invocation is a script or multiple lines in the console + $isMultiLineOrExternal = $myinv.ScriptName -or $myinv.ScriptLineNumber -gt 1 + + # - check that it's not a script module as expectation is that users don't want to see the line of error within a module + $shouldShowLineDetail = ($isParseError -or $isMultiLineOrExternal) -and + $myinv.ScriptName -notmatch '\.psm1$' + + if ($useTargetObject -or $shouldShowLineDetail) { + + if ($useTargetObject) { + $posmsg = "${resetcolor}$($err.TargetObject.File)${newline}" + } + elseif ($myinv.ScriptName) { + if ($env:TERM_PROGRAM -eq 'vscode') { + # If we are running in vscode, we know the file:line:col links are clickable so we use this format + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber):$($myinv.OffsetInLine)${newline}" + } + else { + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber)${newline}" + } + } + else { + $posmsg = "${newline}" + } + + if ($useTargetObject) { + $scriptLineNumber = $err.TargetObject.Line + $scriptLineNumberLength = $err.TargetObject.Line.ToString().Length + } + else { + $scriptLineNumber = $myinv.ScriptLineNumber + $scriptLineNumberLength = $myinv.ScriptLineNumber.ToString().Length + } + + if ($scriptLineNumberLength -gt 4) { + $headerWhitespace = ' ' * ($scriptLineNumberLength - 4) + } + + $lineWhitespace = '' + if ($scriptLineNumberLength -lt 4) { + $lineWhitespace = ' ' * (4 - $scriptLineNumberLength) + } + + $verticalBar = '|' + $posmsg += "${accentColor}${headerWhitespace}Line ${verticalBar}${newline}" + + $highlightLine = '' + if ($useTargetObject) { + $line = $_.TargetObject.LineText.Trim() + $offsetLength = 0 + $offsetInLine = 0 + } + else { + $positionMessage = $myinv.PositionMessage.Split($newline) + $line = $positionMessage[1].Substring(1) # skip the '+' at the start + $highlightLine = $positionMessage[$positionMessage.Count - 1].Substring(1) + $offsetLength = $highlightLine.Trim().Length + $offsetInLine = $highlightLine.IndexOf('~') + } + + if (-not $line.EndsWith($newline)) { + $line += $newline + } + + # don't color the whole line + if ($offsetLength -lt $line.Length - 1) { + $line = $line.Insert($offsetInLine + $offsetLength, $resetColor).Insert($offsetInLine, $accentColor) + } + + $posmsg += "${accentColor}${lineWhitespace}${ScriptLineNumber} ${verticalBar} ${resetcolor}${line}" + $offsetWhitespace = ' ' * $offsetInLine + $prefix = "${accentColor}${headerWhitespace} ${verticalBar} ${errorColor}" + if ($highlightLine -ne '') { + $posMsg += "${prefix}${highlightLine}${newline}" + } + $message = "${prefix}" + } + + if (! $err.ErrorDetails -or ! $err.ErrorDetails.Message) { + if ($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception.Message.Contains("~$newline")) { + # need to parse out the relevant part of the pre-rendered positionmessage + $message += $err.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } + elseif ($err.Exception) { + $message += $err.Exception.Message + } + elseif ($err.Message) { + $message += $err.Message + } + else { + $message += $err.ToString() + } + } + else { + $message += $err.ErrorDetails.Message + } + + # if rendering line information, break up the message if it's wider than the console + if ($myinv -and $myinv.ScriptName -or $err.CategoryInfo.Category -eq 'ParserError') { + $prefixLength = [System.Management.Automation.Internal.StringDecorated]::new($prefix).ContentLength + $prefixVtLength = $prefix.Length - $prefixLength + + # replace newlines in message so it lines up correct + $message = $message.Replace($newline, ' ').Replace("`n", ' ').Replace("`t", ' ') + + $windowWidth = 120 + if ($Host.UI.RawUI -ne $null) { + $windowWidth = $Host.UI.RawUI.WindowSize.Width + } + + if ($windowWidth -gt 0 -and ($message.Length - $prefixVTLength) -gt $windowWidth) { + $sb = [Text.StringBuilder]::new() + $substring = Get-TruncatedString -string $message -length ($windowWidth + $prefixVTLength) + $null = $sb.Append($substring) + $remainingMessage = $message.Substring($substring.Length).Trim() + $null = $sb.Append($newline) + while (($remainingMessage.Length + $prefixLength) -gt $windowWidth) { + $subMessage = $prefix + $remainingMessage + $substring = Get-TruncatedString -string $subMessage -length ($windowWidth + $prefixVtLength) + + if ($substring.Length - $prefix.Length -gt 0) + { + $null = $sb.Append($substring) + $null = $sb.Append($newline) + $remainingMessage = $remainingMessage.Substring($substring.Length - $prefix.Length).Trim() + } + else + { + break + } + } + $null = $sb.Append($prefix + $remainingMessage.Trim()) + $message = $sb.ToString() + } + + $message += $newline + } + + $posmsg += "${errorColor}" + $message + + $reason = 'Error' + if ($err.Exception -and $err.Exception.WasThrownFromThrowStatement) { + $reason = 'Exception' + } + # MyCommand can be the script block, so we don't want to show that so check if it's an actual command + elseif ($myinv.MyCommand -and $myinv.MyCommand.Name -and (Get-Command -Name $myinv.MyCommand -ErrorAction Ignore)) + { + $reason = $myinv.MyCommand + } + # If it's a scriptblock, better to show the command in the scriptblock that had the error + elseif ($err.CategoryInfo.Activity) { + $reason = $err.CategoryInfo.Activity + } + elseif ($myinv.MyCommand) { + $reason = $myinv.MyCommand + } + elseif ($myinv.InvocationName) { + $reason = $myinv.InvocationName + } + elseif ($err.CategoryInfo.Category) { + $reason = $err.CategoryInfo.Category + } + elseif ($err.CategoryInfo.Reason) { + $reason = $err.CategoryInfo.Reason + } + + $errorMsg = 'Error' + + "${errorColor}${reason}: ${posmsg}${resetcolor}" + } + + $myinv = $_.InvocationInfo + $err = $_ + if (!$myinv -and $_.ErrorRecord -and $_.ErrorRecord.InvocationInfo) { + $err = $_.ErrorRecord + $myinv = $err.InvocationInfo + } + + if ($err.FullyQualifiedErrorId -eq 'NativeCommandErrorMessage' -or $err.FullyQualifiedErrorId -eq 'NativeCommandError') { + return "${errorColor}$($err.Exception.Message)${resetcolor}" + } + + if ($ErrorView -eq 'DetailedView') { + $message = Get-Error | Out-String + return "${errorColor}${message}${resetcolor}" + } + + if ($ErrorView -eq 'CategoryView') { + $message = $err.CategoryInfo.GetMessage() + return "${errorColor}${message}${resetcolor}" + } + + $posmsg = '' + if ($ErrorView -eq 'ConciseView') { + $posmsg = Get-ConciseViewPositionMessage + } + elseif ($myinv -and ($myinv.MyCommand -or ($err.CategoryInfo.Category -ne 'ParserError'))) { + $posmsg = $myinv.PositionMessage + if ($posmsg -ne '') { + $posmsg = $newline + $posmsg + } + } + + if ($err.PSMessageDetails) { + $posmsg = ' : ' + $err.PSMessageDetails + $posmsg + } + + if ($ErrorView -eq 'ConciseView') { + $recommendedAction = $_.ErrorDetails.RecommendedAction + if (-not [String]::IsNullOrWhiteSpace($recommendedAction)) { + $recommendedAction = $newline + + ${errorColor} + + ' Recommendation: ' + + $recommendedAction + + ${resetcolor} + } + + if ($err.PSMessageDetails) { + $posmsg = "${errorColor}${posmsg}" + } + return $posmsg + $recommendedAction + } + + $indent = 4 + + $errorCategoryMsg = $err.ErrorCategory_Message + + if ($null -ne $errorCategoryMsg) + { + $indentString = '+ CategoryInfo : ' + $err.ErrorCategory_Message + } + else + { + $indentString = '+ CategoryInfo : ' + $err.CategoryInfo + } + + $posmsg += $newline + $indentString + + $indentString = "+ FullyQualifiedErrorId : " + $err.FullyQualifiedErrorId + $posmsg += $newline + $indentString + + $originInfo = $err.OriginInfo + + if (($null -ne $originInfo) -and ($null -ne $originInfo.PSComputerName)) + { + $indentString = "+ PSComputerName : " + $originInfo.PSComputerName + $posmsg += $newline + $indentString + } + + $finalMsg = if ($err.ErrorDetails.Message) { + $err.ErrorDetails.Message + $posmsg + } else { + $err.Exception.Message + $posmsg + } + + "${errorColor}${finalMsg}${resetcolor}"</ScriptBlock> + </ExpressionBinding> + </CustomItem> + </CustomEntry> + </CustomEntries> + </CustomControl> + </View> + </ViewDefinitions> +</Configuration> \ No newline at end of file diff --git a/Reference/OriginalExceptionView.format.ps1xml b/Reference/OriginalExceptionView.format.ps1xml new file mode 100644 index 0000000..d6e176d --- /dev/null +++ b/Reference/OriginalExceptionView.format.ps1xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<Configuration> + <ViewDefinitions> + <View> + <Name>GetErrorInstance</Name> + <ViewSelectedBy> + <TypeName>System.Management.Automation.ErrorRecord#PSExtendedError</TypeName> + </ViewSelectedBy> + <GroupBy> + <PropertyName>PSErrorIndex</PropertyName> + <Label>ErrorIndex</Label> + </GroupBy> + <CustomControl> + <CustomEntries> + <CustomEntry> + <CustomItem> + <ExpressionBinding> + <ScriptBlock> + Set-StrictMode -Off + + $maxDepth = 10 + $ellipsis = "`u{2026}" + $resetColor = '' + $errorColor = '' + $accentColor = '' + + if ($Host.UI.SupportsVirtualTerminal -and ([string]::IsNullOrEmpty($env:__SuppressAnsiEscapeSequences))) { + $resetColor = $PSStyle.Reset + $errorColor = $psstyle.Formatting.Error + $accentColor = $PSStyle.Formatting.FormatAccent + } + + function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) { + $newline = [Environment]::Newline + $output = [System.Text.StringBuilder]::new() + $prefix = ' ' * $indent + + $expandTypes = @( + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) + + # if object is an Exception, add an ExceptionType property +$ \ No newline at end of file From 82f2d6f898dcfb73aeb76d42480883f8a1340e80 Mon Sep 17 00:00:00 2001 From: Joel Bennett <Jaykul@HuddledMasses.org> Date: Mon, 7 Oct 2024 01:58:25 -0400 Subject: [PATCH 15/15] Fix workflow --- .github/workflows/build.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94cb4da..f7b7d82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,10 @@ jobs: submodules: true fetch-depth: 0 + - name: earthly +build + if: github.ref != 'refs/heads/main' + run: earthly --strict +build + - name: earthly +test if: github.ref != 'refs/heads/main' run: earthly --strict +test @@ -34,18 +38,18 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: ModuleBuilder - path: Modules/ModuleBuilder + name: ErrorView + path: Modules/ErrorView - uses: actions/upload-artifact@v4 with: name: TestResults - path: Modules/ModuleBuilder-TestResults + path: Modules/ErrorView-TestResults - uses: actions/upload-artifact@v4 with: name: Packages - path: Modules/ModuleBuilder-Packages + path: Modules/ErrorView-Packages - name: Upload Tests uses: actions/upload-artifact@v4 @@ -68,8 +72,8 @@ jobs: - name: Download Build Output uses: actions/download-artifact@v4 with: - name: ModuleBuilder - path: Modules/ModuleBuilder + name: ErrorView + path: Modules/ErrorView - name: Download Pester Tests uses: actions/download-artifact@v4 with: @@ -83,15 +87,15 @@ jobs: - uses: PoshCode/Actions/install-requiredmodules@v1 - uses: PoshCode/Actions/pester@v1 with: - codeCoveragePath: Modules/ModuleBuilder - moduleUnderTest: ModuleBuilder + codeCoveragePath: Modules/ErrorView + moduleUnderTest: ErrorView additionalModulePaths: ${{github.workspace}}/Modules - name: Publish Test Results uses: zyborg/dotnet-tests-report@v1 with: test_results_path: results.xml - name: Upload Results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Pester Results path: ${{github.workspace}}/*.xml