|
| 1 | +filter GetConciseViewPositionMessage { |
| 2 | + [CmdletBinding()] |
| 3 | + param( |
| 4 | + [Parameter(ValueFromPipeline)] |
| 5 | + [System.Management.Automation.ErrorRecord] |
| 6 | + $InputObject |
| 7 | + ) |
| 8 | + $err = $InputObject |
| 9 | + $posmsg = '' |
| 10 | + $headerWhitespace = '' |
| 11 | + $offsetWhitespace = '' |
| 12 | + $message = '' |
| 13 | + $prefix = '' |
| 14 | + |
| 15 | + # 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 |
| 16 | + # 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 |
| 17 | + # 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. |
| 18 | + $useTargetObject = $null -ne $err.TargetObject -and |
| 19 | + $err.TargetObject -is [System.Collections.IDictionary] -and |
| 20 | + ([System.Collections.IDictionary]$err.TargetObject).Contains('Line') -and |
| 21 | + ([System.Collections.IDictionary]$err.TargetObject).Contains('LineText') |
| 22 | + |
| 23 | + # The checks here determine if we show line detailed error information: |
| 24 | + # - check if `ParserError` and comes from PowerShell which eventually results in a ParseException, but during this execution it's an ErrorRecord |
| 25 | + $isParseError = $err.CategoryInfo.Category -eq 'ParserError' -and |
| 26 | + $err.Exception -is [System.Management.Automation.ParentContainsErrorRecordException] |
| 27 | + |
| 28 | + # - check if invocation is a script or multiple lines in the console |
| 29 | + $isMultiLineOrExternal = $myinv.ScriptName -or $myinv.ScriptLineNumber -gt 1 |
| 30 | + |
| 31 | + # - 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 |
| 32 | + $shouldShowLineDetail = ($isParseError -or $isMultiLineOrExternal) -and |
| 33 | + $myinv.ScriptName -notmatch '\.psm1$' |
| 34 | + |
| 35 | + if ($useTargetObject -or $shouldShowLineDetail) { |
| 36 | + |
| 37 | + if ($useTargetObject) { |
| 38 | + $posmsg = "${resetcolor}$($err.TargetObject.File)${newline}" |
| 39 | + } elseif ($myinv.ScriptName) { |
| 40 | + if ($env:TERM_PROGRAM -eq 'vscode') { |
| 41 | + # If we are running in vscode, we know the file:line:col links are clickable so we use this format |
| 42 | + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber):$($myinv.OffsetInLine)${newline}" |
| 43 | + } else { |
| 44 | + $posmsg = "${resetcolor}$($myinv.ScriptName):$($myinv.ScriptLineNumber)${newline}" |
| 45 | + } |
| 46 | + } else { |
| 47 | + $posmsg = "${newline}" |
| 48 | + } |
| 49 | + |
| 50 | + if ($useTargetObject) { |
| 51 | + $scriptLineNumber = $err.TargetObject.Line |
| 52 | + $scriptLineNumberLength = $err.TargetObject.Line.ToString().Length |
| 53 | + } else { |
| 54 | + $scriptLineNumber = $myinv.ScriptLineNumber |
| 55 | + $scriptLineNumberLength = $myinv.ScriptLineNumber.ToString().Length |
| 56 | + } |
| 57 | + |
| 58 | + if ($scriptLineNumberLength -gt 4) { |
| 59 | + $headerWhitespace = ' ' * ($scriptLineNumberLength - 4) |
| 60 | + } |
| 61 | + |
| 62 | + $lineWhitespace = '' |
| 63 | + if ($scriptLineNumberLength -lt 4) { |
| 64 | + $lineWhitespace = ' ' * (4 - $scriptLineNumberLength) |
| 65 | + } |
| 66 | + |
| 67 | + $verticalBar = '|' |
| 68 | + $posmsg += "${accentColor}${headerWhitespace}Line ${verticalBar}${newline}" |
| 69 | + |
| 70 | + $highlightLine = '' |
| 71 | + if ($useTargetObject) { |
| 72 | + $line = $_.TargetObject.LineText.Trim() |
| 73 | + $offsetLength = 0 |
| 74 | + $offsetInLine = 0 |
| 75 | + } else { |
| 76 | + $positionMessage = $myinv.PositionMessage.Split($newline) |
| 77 | + $line = $positionMessage[1].Substring(1) # skip the '+' at the start |
| 78 | + $highlightLine = $positionMessage[$positionMessage.Count - 1].Substring(1) |
| 79 | + $offsetLength = $highlightLine.Trim().Length |
| 80 | + $offsetInLine = $highlightLine.IndexOf('~') |
| 81 | + } |
| 82 | + |
| 83 | + if (-not $line.EndsWith($newline)) { |
| 84 | + $line += $newline |
| 85 | + } |
| 86 | + |
| 87 | + # don't color the whole line |
| 88 | + if ($offsetLength -lt $line.Length - 1) { |
| 89 | + $line = $line.Insert($offsetInLine + $offsetLength, $resetColor).Insert($offsetInLine, $accentColor) |
| 90 | + } |
| 91 | + |
| 92 | + $posmsg += "${accentColor}${lineWhitespace}${ScriptLineNumber} ${verticalBar} ${resetcolor}${line}" |
| 93 | + $offsetWhitespace = ' ' * $offsetInLine |
| 94 | + $prefix = "${accentColor}${headerWhitespace} ${verticalBar} ${errorColor}" |
| 95 | + if ($highlightLine -ne '') { |
| 96 | + $posMsg += "${prefix}${highlightLine}${newline}" |
| 97 | + } |
| 98 | + $message = "${prefix}" |
| 99 | + } |
| 100 | + |
| 101 | + if (! $err.ErrorDetails -or ! $err.ErrorDetails.Message) { |
| 102 | + if ($err.CategoryInfo.Category -eq 'ParserError' -and $err.Exception.Message.Contains("~$newline")) { |
| 103 | + # need to parse out the relevant part of the pre-rendered positionmessage |
| 104 | + $message += $err.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] |
| 105 | + } elseif ($err.Exception) { |
| 106 | + $message += $err.Exception.Message |
| 107 | + } elseif ($err.Message) { |
| 108 | + $message += $err.Message |
| 109 | + } else { |
| 110 | + $message += $err.ToString() |
| 111 | + } |
| 112 | + } else { |
| 113 | + $message += $err.ErrorDetails.Message |
| 114 | + } |
| 115 | + |
| 116 | + # if rendering line information, break up the message if it's wider than the console |
| 117 | + if ($myinv -and $myinv.ScriptName -or $err.CategoryInfo.Category -eq 'ParserError') { |
| 118 | + $prefixLength = [System.Management.Automation.Internal.StringDecorated]::new($prefix).ContentLength |
| 119 | + $prefixVtLength = $prefix.Length - $prefixLength |
| 120 | + |
| 121 | + # replace newlines in message so it lines up correct |
| 122 | + $message = $message.Replace($newline, ' ').Replace("`n", ' ').Replace("`t", ' ') |
| 123 | + |
| 124 | + $windowWidth = 120 |
| 125 | + if ($Host.UI.RawUI -ne $null) { |
| 126 | + $windowWidth = $Host.UI.RawUI.WindowSize.Width |
| 127 | + } |
| 128 | + |
| 129 | + if ($windowWidth -gt 0 -and ($message.Length - $prefixVTLength) -gt $windowWidth) { |
| 130 | + $sb = [Text.StringBuilder]::new() |
| 131 | + $substring = TruncateString -string $message -length ($windowWidth + $prefixVTLength) |
| 132 | + $null = $sb.Append($substring) |
| 133 | + $remainingMessage = $message.Substring($substring.Length).Trim() |
| 134 | + $null = $sb.Append($newline) |
| 135 | + while (($remainingMessage.Length + $prefixLength) -gt $windowWidth) { |
| 136 | + $subMessage = $prefix + $remainingMessage |
| 137 | + $substring = TruncateString -string $subMessage -length ($windowWidth + $prefixVtLength) |
| 138 | + |
| 139 | + if ($substring.Length - $prefix.Length -gt 0) { |
| 140 | + $null = $sb.Append($substring) |
| 141 | + $null = $sb.Append($newline) |
| 142 | + $remainingMessage = $remainingMessage.Substring($substring.Length - $prefix.Length).Trim() |
| 143 | + } else { |
| 144 | + break |
| 145 | + } |
| 146 | + } |
| 147 | + $null = $sb.Append($prefix + $remainingMessage.Trim()) |
| 148 | + $message = $sb.ToString() |
| 149 | + } |
| 150 | + |
| 151 | + $message += $newline |
| 152 | + } |
| 153 | + |
| 154 | + $posmsg += "${errorColor}" + $message |
| 155 | + |
| 156 | + $reason = 'Error' |
| 157 | + if ($err.Exception -and $err.Exception.WasThrownFromThrowStatement) { |
| 158 | + $reason = 'Exception' |
| 159 | + # MyCommand can be the script block, so we don't want to show that so check if it's an actual command |
| 160 | + } elseif ($myinv.MyCommand -and $myinv.MyCommand.Name -and (Get-Command -Name $myinv.MyCommand -ErrorAction Ignore)) { |
| 161 | + $reason = $myinv.MyCommand |
| 162 | + } elseif ($err.CategoryInfo.Activity) { |
| 163 | + # If it's a scriptblock, better to show the command in the scriptblock that had the error |
| 164 | + $reason = $err.CategoryInfo.Activity |
| 165 | + } elseif ($myinv.MyCommand) { |
| 166 | + $reason = $myinv.MyCommand |
| 167 | + } elseif ($myinv.InvocationName) { |
| 168 | + $reason = $myinv.InvocationName |
| 169 | + } elseif ($err.CategoryInfo.Category) { |
| 170 | + $reason = $err.CategoryInfo.Category |
| 171 | + } elseif ($err.CategoryInfo.Reason) { |
| 172 | + $reason = $err.CategoryInfo.Reason |
| 173 | + } |
| 174 | + |
| 175 | + "${errorColor}${reason}: ${posmsg}${resetcolor}" |
| 176 | +} |
0 commit comments