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 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/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 @@ + + + + + children + + System.IO.DirectoryInfo + + + PSParentPath + + + + + + 7 + Left + + + + 26 + Right + + + + 14 + Right + + + + Left + + + + + + + + ModeWithoutHardLink + + + LastWriteTimeString + + + LengthString + + + NameString + + + + + + + + childrenWithHardlink + + System.IO.DirectoryInfo + + + PSParentPath + + + + + + 7 + Left + + + + 26 + Right + + + + 14 + Right + + + + Left + + + + + + + + Mode + + + LastWriteTimeString + + + LengthString + + + NameString + + + + + + + + children + + System.IO.DirectoryInfo + + + PSParentPath + + + + + + + Name + + + CreationTime + + + LastWriteTime + + + LastAccessTime + + + Mode + + + LinkType + + + Target + + + + + + System.IO.FileInfo + + + + Name + + + + LengthString + + + CreationTime + + + LastWriteTime + + + LastAccessTime + + + Mode + + + LinkType + + + Target + + + VersionInfo + + + + + + + + children + + System.IO.DirectoryInfo + + + PSParentPath + + + + + + Name + + + + + System.IO.DirectoryInfo + + + Name + + + + + + + \ 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 @@ + \ No newline at end of file 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/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 @@ + + + + + GetErrorInstance + + System.Management.Automation.ErrorRecord#PSExtendedError + + + PSErrorIndex + + + + + + + + + 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 $_ + + + + + + + + + ErrorInstance + + System.Management.Automation.ErrorRecord + + + + + + + + $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 + + + 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}" + + + + + + + + \ 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 @@ + + + + + GetErrorInstance + + System.Management.Automation.ErrorRecord#PSExtendedError + + + PSErrorIndex + + + + + + + + + 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 diff --git a/Reference/default.ps1 b/Reference/default.ps1 new file mode 100644 index 0000000..cae60b6 --- /dev/null +++ b/Reference/default.ps1 @@ -0,0 +1,302 @@ +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 ($null -ne $Host.UI.RawUI) { + $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}" \ No newline at end of file diff --git a/Reference/detail.ps1 b/Reference/detail.ps1 new file mode 100644 index 0000000..f4f1ac8 --- /dev/null +++ b/Reference/detail.ps1 @@ -0,0 +1,196 @@ +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 $_ \ 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/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/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/ErrorView.format.ps1xml b/source/ErrorView.format.ps1xml index 4b3a2c7..582f7f0 100644 --- a/source/ErrorView.format.ps1xml +++ b/source/ErrorView.format.ps1xml @@ -11,12 +11,13 @@ - - Write-NativeCommandError $_ - + + ExceptionInstance + + + System.Exception + + + + + + + + + + + + + + + + + InformationRecord + + + System.Management.Automation.InformationRecord + + + + + + + + $_.MessageData | Format-List * | Out-String + + + + + "Tags: " + @($_.Tags -join ", ") + + + + + $_ | Select-Object * -ExcludeProperty Tags, MessageData | Format-List * -Force | Out-String + + + + + + + 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 9f73337..a116ae8 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1,8 +1,17 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'ErrorView is all about the ErrorView global variable')] param( - $global:ErrorView = "Simple" + $ErrorView ) -# We need to _overwrite_ the ErrorView -# So -PrependPath, instead of FormatsToProcess -Update-FormatData -PrependPath $PSScriptRoot\ErrorView.format.ps1xml \ No newline at end of file +# 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 } + +$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/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/GetConciseMessage.ps1 b/source/private/GetConciseMessage.ps1 new file mode 100644 index 0000000..707194f --- /dev/null +++ b/source/private/GetConciseMessage.ps1 @@ -0,0 +1,174 @@ +filter GetConciseMessage { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + $err = $InputObject + $posmsg = '' + $headerWhitespace = '' + $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}" + $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 ($null -ne $Host.UI.RawUI) { + $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/private/GetErrorMessage.ps1 b/source/private/GetErrorMessage.ps1 new file mode 100644 index 0000000..515abfe --- /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 ($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 + $InputObject.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } elseif ($InputObject.Exception) { + $InputObject.Exception.Message + } elseif ($InputObject.Message) { + $InputObject.Message + } else { + $InputObject.ToString() + } + } +} \ 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/GetListRecursive.ps1 b/source/private/GetListRecursive.ps1 new file mode 100644 index 0000000..ef943ea --- /dev/null +++ b/source/private/GetListRecursive.ps1 @@ -0,0 +1,167 @@ +function GetListRecursive { + <# + .SYNOPSIS + Internal implementation of the Detailed error view to support recursion and indentation + #> + [CmdletBinding()] + param( + $InputObject, + [int]$indent = 0, + [int]$depth = 1 + ) + $output = [System.Text.StringBuilder]::new() + $padding = ' ' * $indent + + $expandTypes = @( + 'Microsoft.Rest.HttpRequestMessageWrapper' + 'Microsoft.Rest.HttpResponseMessageWrapper' + 'System.Management.Automation.InvocationInfo' + ) + + # 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($padding) + $null = $output.Append($accentColor) + $null = $output.Append($prop.Name) + $null = $output.Append(': ') + $null = $output.Append($resetColor) + + [int]$nextIndent = $indent + 2 + [int]$nextDepth = $depth + 1 + $nextPadding = ' ' * $nextIndent + + # 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 + ($null -ne $prop.TypeNames -and $expandTypes -contains $prop.TypeNames[0])) { + + if ($depth -ge $maxDepth) { + $null = $output.Append($ellipsis) + } else { + # For Exceptions, add a fake "Type" property + if ($prop.Value -is [Exception]) { + $null = $output.Append(( $accentColor + "[" + $prop.Value.GetType().FullName + "]" + $resetColor)) + } + $null = $output.Append($newline) + $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 + 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 $nextIndent $nextDepth)) + } + } elseif ($prop.Name -eq 'StackTrace') { + # 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) + $Wrap = @{ + Width = $Host.UI.RawUI.BufferSize.Width - 2 + IndentPadding = "" + WrappedIndent = " " + Colors = $LineColors + } + $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 + $null = $output.Append($newline) + foreach ($key in $prop.Value.Keys) { + if ($key -eq 'Authorization') { + $null = $output.Append("${nextPadding}${accentColor}${key}: ${resetColor}${ellipsis}${newline}") + } else { + $null = $output.Append("${nextPadding}${accentColor}${key}: ${resetColor}$($prop.Value[$key])${newline}") + } + } + } 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 ($value -is [Type]) { + # Just show the typename instead of it as an object + $null = $output.Append("[$($value.ToString())]") + } elseif ($value -is [string] -or $value.GetType().IsPrimitive) { + $null = $output.Append("${value}") + } else { + if (!$isFirstElement) { + $null = $output.Append($newline) + } + $null = $output.Append((GetListRecursive $value $nextIndent $nextDepth)) + } + $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 ($InputObject -is [System.Management.Automation.InvocationInfo] -and $prop.Name -eq 'PositionMessage') { + # Make the underline red + $value = $value.Insert($value.IndexOf('~'), $errorColor) + } elseif ( ($InputObject -is [System.Management.Automation.ErrorRecord] -or + $InputObject -is [System.Exception]) -and $prop.Name -in 'Message', 'FullyQualifiedErrorId', 'CategoryInfo') { + $value = $errorColor + $value + } + $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)) + } + } + + $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..9862626 --- /dev/null +++ b/source/private/GetYamlRecursive.ps1 @@ -0,0 +1,177 @@ +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 = 1, + + # 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 { + $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 + + 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 + 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 -gt $maxDepth } { + "$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)" + "$newline$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) { + "$newline$accentColor$padding$resetColor- $Value" + # } + } + break + } + + # Limit recursive enumeration to specific types: + { $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' + '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]) { + "$newline$padding${accentColor}#Type:$resetColor ${accentColor}" + $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) { + "$newline$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 $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 { + "$($StringValue -replace '''', '''''')" + } + } else { + Write-Warning "Unable to convert $($InputObject.GetType().FullName) to string" + } + } + } + } catch { + "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 "" + } +} \ 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/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 diff --git a/source/private/WrapString.ps1 b/source/private/WrapString.ps1 new file mode 100644 index 0000000..e825dd1 --- /dev/null +++ b/source/private/WrapString.ps1 @@ -0,0 +1,89 @@ + +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 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 = @(''), + + # If set, will output empty lines for each original new line + [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() + $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)") { + 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))" + 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, ''))" + foreach($slice in $slices) { + $lineLength += $slice.Length + if ($lineLength -le $Width) { + Write-Verbose "+ $($slice.Length) = $lineLength < $Width" + $null = $buffer.Append($slice.Text) + } else { + Write-Verbose "Output $($lineLength - $slice.Length)" + Write-Verbose "+ $($slice.Length) = $($slice.Length)" + #$null = $output.Append($buffer.ToString()) + $null = $buffer.Append($newline).Append($WrappedIndent).Append($slice.Text) + $lineLength = $IndentPadding.Length + $slice.Length + } + } + 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() + } +} \ No newline at end of file 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-ConciseErrorView.ps1 b/source/public/ConvertTo-ConciseErrorView.ps1 new file mode 100644 index 0000000..390e6b4 --- /dev/null +++ b/source/public/ConvertTo-ConciseErrorView.ps1 @@ -0,0 +1,32 @@ +function ConvertTo-ConciseErrorView { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject + ) + begin { ResetColor } + process { + if ($InputObject.FullyQualifiedErrorId -in 'NativeCommandErrorMessage','NativeCommandError') { + "${errorColor}$($InputObject.Exception.Message)${resetColor}" + } else { + if (!"$accentColor".Length) { + $local:accentColor = ">>>" + $local:resetColor = "<<<" + } + + $message = GetConciseMessage -InputObject $InputObject + + if ($InputObject.PSMessageDetails) { + $message = $errorColor + ' : ' + $InputObject.PSMessageDetails + $message + } + + $recommendedAction = $InputObject.ErrorDetails.RecommendedAction + if (-not [String]::IsNullOrWhiteSpace($recommendedAction)) { + $message = $message + $newline + ${errorColor} + ' Recommendation: ' + $recommendedAction + ${resetcolor} + } + + $message + } + } +} \ No newline at end of file diff --git a/source/public/ConvertTo-DetailedErrorView.ps1 b/source/public/ConvertTo-DetailedErrorView.ps1 index ed52bcb..539b43f 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,188 +19,13 @@ function ConvertTo-DetailedErrorView { # The maximum depth to recurse into the object [int]$maxDepth = 10 ) - - begin { - - 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 = $PSStyle.Reset - $errorColor = $PSStyle.Formatting.Error - $accentColor = $PSStyle.Formatting.FormatAccent - } - - 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() - } - } + begin { ResetColor } process { - DetailedErrorView $InputObject + $newline + (GetListRecursive $InputObject) + $newline + if ($Env:GITHUB_ACTIONS) { + "::error $(GetGoogleWorkflowPositionMesage),title=$(GetErrorTitle $InputObject)::$(GetErrorMessage $InputObject)" + } elseif ($Env:TF_BUILD) { + "##vso[task.logissue type=error;$(GetAzurePipelinesPositionMesage)]$(GetErrorTitle $InputObject): $(GetErrorMessage $InputObject)" + } } -} +} \ No newline at end of file diff --git a/source/public/ConvertTo-FullErrorView.ps1 b/source/public/ConvertTo-FullErrorView.ps1 index dbe4ce5..c13705e 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,32 @@ 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" } + } + + $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..9ee9113 100644 --- a/source/public/ConvertTo-NormalErrorView.ps1 +++ b/source/public/ConvertTo-NormalErrorView.ps1 @@ -11,64 +11,48 @@ filter ConvertTo-NormalErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) - - if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { - $InputObject.Exception.Message - } 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 = "+ CategoryInfo : " + $InputObject.ErrorCategory_Message - } else { - $indentString = "+ CategoryInfo : " + $InputObject.CategoryInfo - } - $posmsg += "`n" - foreach ($line in @($indentString -split "(.{$width})")) { - if ($line) { - $posmsg += (" " * $indent + $line) + $Wrap = @{ + Width = $host.UI.RawUI.BufferSize.Width - 2 + IndentPadding = " " } - } - $indentString = "+ FullyQualifiedErrorId : " + $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) { - $InputObject.Exception.Message + $posmsg + "`n " - } else { - $InputObject.ErrorDetails.Message + $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 new file mode 100644 index 0000000..31de80f --- /dev/null +++ b/source/public/ConvertTo-NormalExceptionView.ps1 @@ -0,0 +1,18 @@ +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 + ) + begin { ResetColor } + process { + $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 20e4849..c8b836b 100644 --- a/source/public/ConvertTo-SimpleErrorView.ps1 +++ b/source/public/ConvertTo-SimpleErrorView.ps1 @@ -4,29 +4,27 @@ function ConvertTo-SimpleErrorView { [System.Management.Automation.ErrorRecord] $InputObject ) + $resetColor = '' + $errorColor = '' + #$accentColor = '' - if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { + 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 -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 } @@ -46,9 +44,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/ConvertTo-YamlErrorView.ps1 b/source/public/ConvertTo-YamlErrorView.ps1 new file mode 100644 index 0000000..089e314 --- /dev/null +++ b/source/public/ConvertTo-YamlErrorView.ps1 @@ -0,0 +1,26 @@ +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. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'maxDepth')] + [CmdletBinding()] + param( + # The object that you want to convert to YAML + [Parameter(Mandatory, ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $InputObject, + + # The maximum depth to recurse into the object + [int]$maxDepth = 10, + + # If set, include empty and null properties in the output + [switch]$IncludeEmpty + ) + begin { ResetColor } + process { + GetYamlRecursive -InputObject $InputObject -IncludeEmpty:$IncludeEmpty + } +} \ No newline at end of file diff --git a/source/public/Format-Error.ps1 b/source/public/Format-Error.ps1 index 59a80c8..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,37 +28,41 @@ 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", - [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 + # Encourages ErrorView functions to recurse InnerException properties [switch]$Recurse ) begin { $ErrorActionPreference = "Continue" - $View, $ErrorView = $ErrorView, $View - [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/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/Set-ErrorView.ps1 b/source/public/Set-ErrorView.ps1 index cd52ea6..7a0d5e6 100644 --- a/source/public/Set-ErrorView.ps1 +++ b/source/public/Set-ErrorView.ps1 @@ -21,13 +21,14 @@ 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 $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 deleted file mode 100644 index 08b699f..0000000 --- a/source/public/Write-NativeCommandError.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -function Write-NativeCommandError { - [CmdletBinding()] - param( - [System.Management.Automation.ErrorRecord] - $InputObject - ) - - if ($InputObject.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { return } - - $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