diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94cb4da..8b2499b 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,24 +38,24 @@ 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 with: name: PesterTests - path: ${{github.workspace}}/Tests + path: ${{github.workspace}}/tests - name: Upload RequiredModules.psd1 uses: actions/upload-artifact@v4 with: @@ -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/.gitignore b/.gitignore index f5e556f..40fae01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ output/ Modules/ +# Ignore version number folders (I put my module in these so I can use it) +/[0-9]*/ \ No newline at end of file diff --git a/Build.build.ps1 b/Build.build.ps1 index 5fbe9f6..ec333b3 100644 --- a/Build.build.ps1 +++ b/Build.build.ps1 @@ -31,7 +31,8 @@ $ErrorView = 'DetailedView' # The name of the module to publish $script:PSModuleName = "TerminalBlocks" -$script:RequiredCodeCoverage = 0.85 +# Pester doesn't measure the code coverage properly, because we test things by running in a new session +$script:RequiredCodeCoverage = 0.0 # Use Env because then Earthly can override it $Env:OUTPUT_ROOT ??= Join-Path $BuildRoot Modules diff --git a/Earthfile b/Earthfile index a166ad5..3ea1ad2 100644 --- a/Earthfile +++ b/Earthfile @@ -8,7 +8,7 @@ ARG --global EARTHLY_BUILD_SHA ARG --global EARTHLY_GIT_BRANCH # These are my common paths, used in my shared /Tasks repo ARG --global OUTPUT_ROOT=/Modules -ARG --global TEST_ROOT=/Tests +ARG --global TEST_ROOT=/tests ARG --global TEMP_ROOT=/temp # These are my common build args, used in my shared /Tasks repo ARG --global MODULE_NAME=ErrorView @@ -59,7 +59,7 @@ test: # BUILD +build FROM +build # Copy the test files here, so we can avoid rebuilding when iterating on tests - COPY --if-exists --dir Tests/ ScriptAnalyzerSettings.psd1 /work + COPY --if-exists --dir $TEST_ROOT ScriptAnalyzerSettings.psd1 /work RUN ["pwsh", "-Command", "Invoke-Build", "-Task", "Test", "-File", "Build.build.ps1"] # SAVE ARTIFACT [--keep-ts] [--keep-own] [--if-exists] [--force] [] [AS LOCAL ] 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..b42adb3 100644 --- a/source/ErrorView.format.ps1xml +++ b/source/ErrorView.format.ps1xml @@ -6,23 +6,15 @@ System.Management.Automation.ErrorRecord + System.Exception - - Write-NativeCommandError $_ - - + Format-Error -View $global:ErrorView -InputObject $_ diff --git a/source/postfix.ps1 b/source/postfix.ps1 new file mode 100644 index 0000000..0c4a5a9 --- /dev/null +++ b/source/postfix.ps1 @@ -0,0 +1,7 @@ +if ($ErrorViewArgument) { + Set-ErrorView $ErrorViewArgument +} 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..52b13a1 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1,8 +1,56 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'ErrorView is all about the ErrorView global variable')] param( - $global:ErrorView = "Simple" + $ErrorViewArgument ) -# 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" +) + +$Escapes = @( + '\u001B' + '\u009B' +) + +# From https://github.com/chalk/wrap-ansi + +# $AnsiEnd = 39 +$AnsiBell = '\u0007' +$AnsiCSI = '[' +$AnsiOsc = ']' +$AnsiSgrTerminator = 'm' +$AnsiEscapeLink = "${AnsiOsc}8;;" + +function wrapAnsiCsiCode($code) { "$(${Escapes}[0])${AnsiCsi}${code}${AnsiSgrTerminator}" } +function wrapAnsiHyperlink($url) { "$(${Escapes}[0])${AnsiEscapeLink}${url}${AnsiBell}" } + + +# https://github.com/chalk/ansi-regex +# Valid string terminator sequences are BEL, ESC\, and 0x9c +$ST = '(?:\u0007|\u001b\u005c|\u009c)' +$script:AnsiPattern = @( + "[\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*" + "[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?${ST})" + '(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))' + ) -join '|' +$script:AnsiRegex = [Regex]::new($AnsiPattern, "Compiled"); +# $script:WordBoundaryRegex = [Regex]::new("((?:$AnsiPattern)*[^ -,.?!:;=\n\r\t\\\|\/]+?(?:$AnsiPattern)*)", "Compiled") +$script:WordBoundaryRegex = [Regex]::new("((?:(?:$AnsiPattern)*[^ -,.?!:;=\n\r\t\\\|\/]+?(?:$AnsiPattern)*)|[ -,.?!:;=\n\r\t\\\|\/])", "Compiled") +#$script:AnsiSplitRegex = [Regex]::new(, "Compiled") + +# https://github.com/mathiasbynens/emoji-regex +$script:EmojiPattern = "[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE89\uDE8F-\uDEC2\uDEC6\uDECE-\uDEDC\uDEDF-\uDEE9]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)" + + +$script:EmojiPattern = "\uD83D(?:\uDC68(?:\uD83C(?:\uDFFB(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFC(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFD(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFE(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFF(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?)|\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83D(?:(?:[\uDC68\uDC69]\u200D\uD83D)?(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]))?|\uDC69(?:\uD83C(?:\uDFFB(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D[\uDC68\uDC69]|[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFC(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D[\uDC68\uDC69]|[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFD(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D[\uDC68\uDC69]|[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFE(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D[\uDC68\uDC69]|[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFF(?:\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D[\uDC68\uDC69]|[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?)|\u200D(?:\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D[\uDC68\uDC69]|[\uDC68\uDC69])|\uD83D(?:(?:\uDC69\u200D\uD83D)?(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]))?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF](?:\u200D(?:[\u2640\u2642]\uFE0F(?:\u200D\u27A1\uFE0F?)?|(?:[\u2640\u2642]\u200D)?\u27A1\uFE0F?|[\u2640\u2642]))?|\u200D(?:[\u2640\u2642]\uFE0F(?:\u200D\u27A1\uFE0F?)?|(?:[\u2640\u2642]\u200D)?\u27A1\uFE0F?|[\u2640\u2642]))?|(?:\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|\uDC6F)(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?|\u200D[\u2640\u2642]\uFE0F?)?|\uDC41(?:\uFE0F(?:\u200D\uD83D\uDDE8\uFE0F?)?|\u200D\uD83D\uDDE8\uFE0F?)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\uD83D\uDD25|\u2B1B))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD74\uDD90]\uD83C[\uDFFB-\uDFFF]|\uDC08(?:\u200D\u2B1B)?|[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD74\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDD90\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0])|\uD83E(?:\uDDD1(?:\uD83C(?:\uDFFB(?:\u200D(?:\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFC(?:\u200D(?:\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFD(?:\u200D(?:\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFE(?:\u200D(?:\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDFFF(?:\u200D(?:\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?)|\u200D(?:\uD83E(?:(?:\uDDD1\u200D\uD83E)?\uDDD2(?:\u200D\uD83E\uDDD2)?|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|\uDD1D\u200D\uD83E\uDDD1|[\uDDB0-\uDDB3])|[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))?|\uDDCE(?:\uD83C[\uDFFB-\uDFFF](?:\u200D(?:[\u2640\u2642]\uFE0F(?:\u200D\u27A1\uFE0F?)?|(?:[\u2640\u2642]\u200D)?\u27A1\uFE0F?|[\u2640\u2642]))?|\u200D(?:[\u2640\u2642]\uFE0F(?:\u200D\u27A1\uFE0F?)?|(?:[\u2640\u2642]\u200D)?\u27A1\uFE0F?|[\u2640\u2642]))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?|\u200D[\u2640\u2642]\uFE0F?)?|[\uDD3C\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE89\uDE8F-\uDEC2\uDEC6\uDECE-\uDEDC\uDEDF-\uDEE9])|\uD83C(?:\uDFF4(?:\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F|\u200D\u2620\uFE0F?)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF](?:\u200D(?:[\u2640\u2642]\uFE0F(?:\u200D\u27A1\uFE0F?)?|(?:[\u2640\u2642]\u200D)?\u27A1\uFE0F?|[\u2640\u2642]))?|\u200D(?:[\u2640\u2642]\uFE0F(?:\u200D\u27A1\uFE0F?)?|(?:[\u2640\u2642]\u200D)?\u27A1\uFE0F?|[\u2640\u2642]))?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?|\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDFF3(?:\uFE0F(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|(?:[\uDFCB\uDFCC]\u200D[\u2640\u2642]|[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7])\uFE0F?|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCB\uDFCC\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF])|\u26F9(?:(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)(?:\u200D[\u2640\u2642]\uFE0F?)?|\u200D[\u2640\u2642]\uFE0F?)?|\u26D3(?:\uFE0F(?:\u200D\uD83D\uDCA5)?|\u200D\uD83D\uDCA5)?|\u2764(?:\uFE0F(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|[\#\*0-9]\uFE0F?\u20E3|[\u261D\u270C\u270D]\uD83C[\uDFFB-\uDFFF]|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u00A9\u00AE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270C\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]\uFE0F?|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]" + +$script:EmojiRegex = [Regex]::new($EmojiPattern, "Compiled"); \ 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..bed6586 --- /dev/null +++ b/source/private/GetConciseMessage.ps1 @@ -0,0 +1,174 @@ +filter GetConciseMessage { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [System.Management.Automation.ErrorRecord] + $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 $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') + + # 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 = $InputObject.CategoryInfo.Category -eq 'ParserError' -and + $InputObject.Exception -is [System.Management.Automation.ParentContainsErrorRecordException] + + # - check if invocation is a script or multiple lines in the console + $Invocation = $InputObject.InvocationInfo + $isMultiLineOrExternal = $Invocation.ScriptName -or $Invocation.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 + $Invocation.ScriptName -notmatch '\.psm1$' + + if ($useTargetObject -or $shouldShowLineDetail) { + + if ($useTargetObject) { + $posmsg = "${resetcolor}$($InputObject.TargetObject.File)${newline}" + } elseif ($Invocation.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}$($Invocation.ScriptName):$($Invocation.ScriptLineNumber):$($Invocation.OffsetInLine)${newline}" + } else { + $posmsg = "${resetcolor}$($Invocation.ScriptName):$($Invocation.ScriptLineNumber)${newline}" + } + } else { + $posmsg = "${newline}" + } + + if ($useTargetObject) { + $scriptLineNumber = $InputObject.TargetObject.Line + $scriptLineNumberLength = $InputObject.TargetObject.Line.ToString().Length + } else { + $scriptLineNumber = $Invocation.ScriptLineNumber + $scriptLineNumberLength = $Invocation.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 = $Invocation.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 (! $InputObject.ErrorDetails -or ! $InputObject.ErrorDetails.Message) { + if ($InputObject.CategoryInfo.Category -eq 'ParserError' -and $InputObject.Exception.Message.Contains("~$newline")) { + # need to parse out the relevant part of the pre-rendered positionmessage + $message += $InputObject.Exception.Message.split("~$newline")[1].split("${newline}${newline}")[0] + } elseif ($InputObject.Exception) { + $message += $InputObject.Exception.Message + } elseif ($InputObject.Message) { + $message += $InputObject.Message + } else { + $message += $InputObject.ToString() + } + } else { + $message += $InputObject.ErrorDetails.Message + } + + # if rendering line information, break up the message if it's wider than the console + if ($Invocation -and $Invocation.ScriptName -or $InputObject.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 ($InputObject.Exception -and $InputObject.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 ($Invocation.MyCommand -and $Invocation.MyCommand.Name -and (Get-Command -Name $Invocation.MyCommand -ErrorAction Ignore)) { + $reason = $Invocation.MyCommand + } elseif ($InputObject.CategoryInfo.Activity) { + # If it's a scriptblock, better to show the command in the scriptblock that had the error + $reason = $InputObject.CategoryInfo.Activity + } elseif ($Invocation.MyCommand) { + $reason = $Invocation.MyCommand + } elseif ($Invocation.InvocationName) { + $reason = $Invocation.InvocationName + } elseif ($InputObject.CategoryInfo.Category) { + $reason = $InputObject.CategoryInfo.Category + } elseif ($InputObject.CategoryInfo.Reason) { + $reason = $InputObject.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/GetErrorPrefix.ps1 b/source/private/GetErrorPrefix.ps1 new file mode 100644 index 0000000..bcdd8a1 --- /dev/null +++ b/source/private/GetErrorPrefix.ps1 @@ -0,0 +1,43 @@ +filter GetErrorPrefix { + [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/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..a5e251e --- /dev/null +++ b/source/private/GetListRecursive.ps1 @@ -0,0 +1,166 @@ +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() +} 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/Graphemes.ps1 b/source/private/Graphemes.ps1 new file mode 100644 index 0000000..f9b1cc8 --- /dev/null +++ b/source/private/Graphemes.ps1 @@ -0,0 +1,44 @@ +# https://github.com/sindresorhus/get-east-asian-width +function Test-Ambiguous([int]$x) { + return $x -eq 0xA1 -or $x -eq 0xA4 -or $x -eq 0xA7 -or $x -eq 0xA8 -or $x -eq 0xAA -or $x -eq 0xAD -or $x -eq 0xAE -or $x -ge 0xB0 -and $x -le 0xB4 -or $x -ge 0xB6 -and $x -le 0xBA -or $x -ge 0xBC -and $x -le 0xBF -or $x -eq 0xC6 -or $x -eq 0xD0 -or $x -eq 0xD7 -or $x -eq 0xD8 -or $x -ge 0xDE -and $x -le 0xE1 -or $x -eq 0xE6 -or $x -ge 0xE8 -and $x -le 0xEA -or $x -eq 0xEC -or $x -eq 0xED -or $x -eq 0xF0 -or $x -eq 0xF2 -or $x -eq 0xF3 -or $x -ge 0xF7 -and $x -le 0xFA -or $x -eq 0xFC -or $x -eq 0xFE -or $x -eq 0x101 -or $x -eq 0x111 -or $x -eq 0x113 -or $x -eq 0x11B -or $x -eq 0x126 -or $x -eq 0x127 -or $x -eq 0x12B -or $x -ge 0x131 -and $x -le 0x133 -or $x -eq 0x138 -or $x -ge 0x13F -and $x -le 0x142 -or $x -eq 0x144 -or $x -ge 0x148 -and $x -le 0x14B -or $x -eq 0x14D -or $x -eq 0x152 -or $x -eq 0x153 -or $x -eq 0x166 -or $x -eq 0x167 -or $x -eq 0x16B -or $x -eq 0x1CE -or $x -eq 0x1D0 -or $x -eq 0x1D2 -or $x -eq 0x1D4 -or $x -eq 0x1D6 -or $x -eq 0x1D8 -or $x -eq 0x1DA -or $x -eq 0x1DC -or $x -eq 0x251 -or $x -eq 0x261 -or $x -eq 0x2C4 -or $x -eq 0x2C7 -or $x -ge 0x2C9 -and $x -le 0x2CB -or $x -eq 0x2CD -or $x -eq 0x2D0 -or $x -ge 0x2D8 -and $x -le 0x2DB -or $x -eq 0x2DD -or $x -eq 0x2DF -or $x -ge 0x300 -and $x -le 0x36F -or $x -ge 0x391 -and $x -le 0x3A1 -or $x -ge 0x3A3 -and $x -le 0x3A9 -or $x -ge 0x3B1 -and $x -le 0x3C1 -or $x -ge 0x3C3 -and $x -le 0x3C9 -or $x -eq 0x401 -or $x -ge 0x410 -and $x -le 0x44F -or $x -eq 0x451 -or $x -eq 0x2010 -or $x -ge 0x2013 -and $x -le 0x2016 -or $x -eq 0x2018 -or $x -eq 0x2019 -or $x -eq 0x201C -or $x -eq 0x201D -or $x -ge 0x2020 -and $x -le 0x2022 -or $x -ge 0x2024 -and $x -le 0x2027 -or $x -eq 0x2030 -or $x -eq 0x2032 -or $x -eq 0x2033 -or $x -eq 0x2035 -or $x -eq 0x203B -or $x -eq 0x203E -or $x -eq 0x2074 -or $x -eq 0x207F -or $x -ge 0x2081 -and $x -le 0x2084 -or $x -eq 0x20AC -or $x -eq 0x2103 -or $x -eq 0x2105 -or $x -eq 0x2109 -or $x -eq 0x2113 -or $x -eq 0x2116 -or $x -eq 0x2121 -or $x -eq 0x2122 -or $x -eq 0x2126 -or $x -eq 0x212B -or $x -eq 0x2153 -or $x -eq 0x2154 -or $x -ge 0x215B -and $x -le 0x215E -or $x -ge 0x2160 -and $x -le 0x216B -or $x -ge 0x2170 -and $x -le 0x2179 -or $x -eq 0x2189 -or $x -ge 0x2190 -and $x -le 0x2199 -or $x -eq 0x21B8 -or $x -eq 0x21B9 -or $x -eq 0x21D2 -or $x -eq 0x21D4 -or $x -eq 0x21E7 -or $x -eq 0x2200 -or $x -eq 0x2202 -or $x -eq 0x2203 -or $x -eq 0x2207 -or $x -eq 0x2208 -or $x -eq 0x220B -or $x -eq 0x220F -or $x -eq 0x2211 -or $x -eq 0x2215 -or $x -eq 0x221A -or $x -ge 0x221D -and $x -le 0x2220 -or $x -eq 0x2223 -or $x -eq 0x2225 -or $x -ge 0x2227 -and $x -le 0x222C -or $x -eq 0x222E -or $x -ge 0x2234 -and $x -le 0x2237 -or $x -eq 0x223C -or $x -eq 0x223D -or $x -eq 0x2248 -or $x -eq 0x224C -or $x -eq 0x2252 -or $x -eq 0x2260 -or $x -eq 0x2261 -or $x -ge 0x2264 -and $x -le 0x2267 -or $x -eq 0x226A -or $x -eq 0x226B -or $x -eq 0x226E -or $x -eq 0x226F -or $x -eq 0x2282 -or $x -eq 0x2283 -or $x -eq 0x2286 -or $x -eq 0x2287 -or $x -eq 0x2295 -or $x -eq 0x2299 -or $x -eq 0x22A5 -or $x -eq 0x22BF -or $x -eq 0x2312 -or $x -ge 0x2460 -and $x -le 0x24E9 -or $x -ge 0x24EB -and $x -le 0x254B -or $x -ge 0x2550 -and $x -le 0x2573 -or $x -ge 0x2580 -and $x -le 0x258F -or $x -ge 0x2592 -and $x -le 0x2595 -or $x -eq 0x25A0 -or $x -eq 0x25A1 -or $x -ge 0x25A3 -and $x -le 0x25A9 -or $x -eq 0x25B2 -or $x -eq 0x25B3 -or $x -eq 0x25B6 -or $x -eq 0x25B7 -or $x -eq 0x25BC -or $x -eq 0x25BD -or $x -eq 0x25C0 -or $x -eq 0x25C1 -or $x -ge 0x25C6 -and $x -le 0x25C8 -or $x -eq 0x25CB -or $x -ge 0x25CE -and $x -le 0x25D1 -or $x -ge 0x25E2 -and $x -le 0x25E5 -or $x -eq 0x25EF -or $x -eq 0x2605 -or $x -eq 0x2606 -or $x -eq 0x2609 -or $x -eq 0x260E -or $x -eq 0x260F -or $x -eq 0x261C -or $x -eq 0x261E -or $x -eq 0x2640 -or $x -eq 0x2642 -or $x -eq 0x2660 -or $x -eq 0x2661 -or $x -ge 0x2663 -and $x -le 0x2665 -or $x -ge 0x2667 -and $x -le 0x266A -or $x -eq 0x266C -or $x -eq 0x266D -or $x -eq 0x266F -or $x -eq 0x269E -or $x -eq 0x269F -or $x -eq 0x26BF -or $x -ge 0x26C6 -and $x -le 0x26CD -or $x -ge 0x26CF -and $x -le 0x26D3 -or $x -ge 0x26D5 -and $x -le 0x26E1 -or $x -eq 0x26E3 -or $x -eq 0x26E8 -or $x -eq 0x26E9 -or $x -ge 0x26EB -and $x -le 0x26F1 -or $x -eq 0x26F4 -or $x -ge 0x26F6 -and $x -le 0x26F9 -or $x -eq 0x26FB -or $x -eq 0x26FC -or $x -eq 0x26FE -or $x -eq 0x26FF -or $x -eq 0x273D -or $x -ge 0x2776 -and $x -le 0x277F -or $x -ge 0x2B56 -and $x -le 0x2B59 -or $x -ge 0x3248 -and $x -le 0x324F -or $x -ge 0xE000 -and $x -le 0xF8FF -or $x -ge 0xFE00 -and $x -le 0xFE0F -or $x -eq 0xFFFD -or $x -ge 0x1F100 -and $x -le 0x1F10A -or $x -ge 0x1F110 -and $x -le 0x1F12D -or $x -ge 0x1F130 -and $x -le 0x1F169 -or $x -ge 0x1F170 -and $x -le 0x1F18D -or $x -eq 0x1F18F -or $x -eq 0x1F190 -or $x -ge 0x1F19B -and $x -le 0x1F1AC -or $x -ge 0xE0100 -and $x -le 0xE01EF -or $x -ge 0xF0000 -and $x -le 0xFFFFD -or $x -ge 0x100000 -and $x -le 0x10FFFD +} + +function Test-FullWidth([int]$x) { + return $x -eq 0x3000 -or $x -ge 0xFF01 -and $x -le 0xFF60 -or $x -ge 0xFFE0 -and $x -le 0xFFE6 +} + +function Test-Wide([int]$x) { + return $x -ge 0x1100 -and $x -le 0x115F -or $x -eq 0x231A -or $x -eq 0x231B -or $x -eq 0x2329 -or $x -eq 0x232A -or $x -ge 0x23E9 -and $x -le 0x23EC -or $x -eq 0x23F0 -or $x -eq 0x23F3 -or $x -eq 0x25FD -or $x -eq 0x25FE -or $x -eq 0x2614 -or $x -eq 0x2615 -or $x -ge 0x2630 -and $x -le 0x2637 -or $x -ge 0x2648 -and $x -le 0x2653 -or $x -eq 0x267F -or $x -ge 0x268A -and $x -le 0x268F -or $x -eq 0x2693 -or $x -eq 0x26A1 -or $x -eq 0x26AA -or $x -eq 0x26AB -or $x -eq 0x26BD -or $x -eq 0x26BE -or $x -eq 0x26C4 -or $x -eq 0x26C5 -or $x -eq 0x26CE -or $x -eq 0x26D4 -or $x -eq 0x26EA -or $x -eq 0x26F2 -or $x -eq 0x26F3 -or $x -eq 0x26F5 -or $x -eq 0x26FA -or $x -eq 0x26FD -or $x -eq 0x2705 -or $x -eq 0x270A -or $x -eq 0x270B -or $x -eq 0x2728 -or $x -eq 0x274C -or $x -eq 0x274E -or $x -ge 0x2753 -and $x -le 0x2755 -or $x -eq 0x2757 -or $x -ge 0x2795 -and $x -le 0x2797 -or $x -eq 0x27B0 -or $x -eq 0x27BF -or $x -eq 0x2B1B -or $x -eq 0x2B1C -or $x -eq 0x2B50 -or $x -eq 0x2B55 -or $x -ge 0x2E80 -and $x -le 0x2E99 -or $x -ge 0x2E9B -and $x -le 0x2EF3 -or $x -ge 0x2F00 -and $x -le 0x2FD5 -or $x -ge 0x2FF0 -and $x -le 0x2FFF -or $x -ge 0x3001 -and $x -le 0x303E -or $x -ge 0x3041 -and $x -le 0x3096 -or $x -ge 0x3099 -and $x -le 0x30FF -or $x -ge 0x3105 -and $x -le 0x312F -or $x -ge 0x3131 -and $x -le 0x318E -or $x -ge 0x3190 -and $x -le 0x31E5 -or $x -ge 0x31EF -and $x -le 0x321E -or $x -ge 0x3220 -and $x -le 0x3247 -or $x -ge 0x3250 -and $x -le 0xA48C -or $x -ge 0xA490 -and $x -le 0xA4C6 -or $x -ge 0xA960 -and $x -le 0xA97C -or $x -ge 0xAC00 -and $x -le 0xD7A3 -or $x -ge 0xF900 -and $x -le 0xFAFF -or $x -ge 0xFE10 -and $x -le 0xFE19 -or $x -ge 0xFE30 -and $x -le 0xFE52 -or $x -ge 0xFE54 -and $x -le 0xFE66 -or $x -ge 0xFE68 -and $x -le 0xFE6B -or $x -ge 0x16FE0 -and $x -le 0x16FE4 -or $x -eq 0x16FF0 -or $x -eq 0x16FF1 -or $x -ge 0x17000 -and $x -le 0x187F7 -or $x -ge 0x18800 -and $x -le 0x18CD5 -or $x -ge 0x18CFF -and $x -le 0x18D08 -or $x -ge 0x1AFF0 -and $x -le 0x1AFF3 -or $x -ge 0x1AFF5 -and $x -le 0x1AFFB -or $x -eq 0x1AFFD -or $x -eq 0x1AFFE -or $x -ge 0x1B000 -and $x -le 0x1B122 -or $x -eq 0x1B132 -or $x -ge 0x1B150 -and $x -le 0x1B152 -or $x -eq 0x1B155 -or $x -ge 0x1B164 -and $x -le 0x1B167 -or $x -ge 0x1B170 -and $x -le 0x1B2FB -or $x -ge 0x1D300 -and $x -le 0x1D356 -or $x -ge 0x1D360 -and $x -le 0x1D376 -or $x -eq 0x1F004 -or $x -eq 0x1F0CF -or $x -eq 0x1F18E -or $x -ge 0x1F191 -and $x -le 0x1F19A -or $x -ge 0x1F200 -and $x -le 0x1F202 -or $x -ge 0x1F210 -and $x -le 0x1F23B -or $x -ge 0x1F240 -and $x -le 0x1F248 -or $x -eq 0x1F250 -or $x -eq 0x1F251 -or $x -ge 0x1F260 -and $x -le 0x1F265 -or $x -ge 0x1F300 -and $x -le 0x1F320 -or $x -ge 0x1F32D -and $x -le 0x1F335 -or $x -ge 0x1F337 -and $x -le 0x1F37C -or $x -ge 0x1F37E -and $x -le 0x1F393 -or $x -ge 0x1F3A0 -and $x -le 0x1F3CA -or $x -ge 0x1F3CF -and $x -le 0x1F3D3 -or $x -ge 0x1F3E0 -and $x -le 0x1F3F0 -or $x -eq 0x1F3F4 -or $x -ge 0x1F3F8 -and $x -le 0x1F43E -or $x -eq 0x1F440 -or $x -ge 0x1F442 -and $x -le 0x1F4FC -or $x -ge 0x1F4FF -and $x -le 0x1F53D -or $x -ge 0x1F54B -and $x -le 0x1F54E -or $x -ge 0x1F550 -and $x -le 0x1F567 -or $x -eq 0x1F57A -or $x -eq 0x1F595 -or $x -eq 0x1F596 -or $x -eq 0x1F5A4 -or $x -ge 0x1F5FB -and $x -le 0x1F64F -or $x -ge 0x1F680 -and $x -le 0x1F6C5 -or $x -eq 0x1F6CC -or $x -ge 0x1F6D0 -and $x -le 0x1F6D2 -or $x -ge 0x1F6D5 -and $x -le 0x1F6D7 -or $x -ge 0x1F6DC -and $x -le 0x1F6DF -or $x -eq 0x1F6EB -or $x -eq 0x1F6EC -or $x -ge 0x1F6F4 -and $x -le 0x1F6FC -or $x -ge 0x1F7E0 -and $x -le 0x1F7EB -or $x -eq 0x1F7F0 -or $x -ge 0x1F90C -and $x -le 0x1F93A -or $x -ge 0x1F93C -and $x -le 0x1F945 -or $x -ge 0x1F947 -and $x -le 0x1F9FF -or $x -ge 0x1FA70 -and $x -le 0x1FA7C -or $x -ge 0x1FA80 -and $x -le 0x1FA89 -or $x -ge 0x1FA8F -and $x -le 0x1FAC6 -or $x -ge 0x1FACE -and $x -le 0x1FADC -or $x -ge 0x1FADF -and $x -le 0x1FAE9 -or $x -ge 0x1FAF0 -and $x -le 0x1FAF8 -or $x -ge 0x20000 -and $x -le 0x2FFFD -or $x -ge 0x30000 -and $x -le 0x3FFFD; +} + +function Get-Category([int]$x) { + if (Test-Ambiguous $x) { return 'ambiguous' } + + if (Test-FullWidth $x) { return 'fullwidth' } + + if ($x -eq 0x20A9 -or $x -ge 0xFF61 -and $x -le 0xFFBE -or $x -ge 0xFFC2 -and $x -le 0xFFC7 -or $x -ge 0xFFCA -and $x -le 0xFFCF -or $x -ge 0xFFD2 -and $x -le 0xFFD7 -or $x -ge 0xFFDA -and $x -le 0xFFDC -or $x -ge 0xFFE8 -and $x -le 0xFFEE) { + return 'halfwidth' + } + + if ($x -ge 0x20 -and $x -le 0x7E -or $x -eq 0xA2 -or $x -eq 0xA3 -or $x -eq 0xA5 -or $x -eq 0xA6 -or $x -eq 0xAC -or $x -eq 0xAF -or $x -ge 0x27E6 -and $x -le 0x27ED -or $x -eq 0x2985 -or $x -eq 0x2986) { + return 'narrow' + } + + if (Test-Wide $x) { return 'wide' } + + return 'neutral' +} +function Get-EastAsianWidthType([char]$codePoint) { + Get-Category $codePoint +} + +function Measure-EastAsianWidth([char]$codePoint, [switch]$AmbiguousAsWide) { + if ( (Test-FullWidth $codePoint) -or (Test-Wide $codePoint) -or ($AmbiguousAsWide -and (Test-Ambiguous $codePoint)) ) { + return 2; + } + + return 1; +} + +# For Prettier. This doesn't count "ambiguous" characters or check for valid input. +function Test-NarrowWidth([char]$codePoint) { !(Test-FullWidth $codePoint) -or (Test-Wide $codePoint) } \ No newline at end of file diff --git a/source/private/ResetColor.ps1 b/source/private/ResetColor.ps1 new file mode 100644 index 0000000..4a831a5 --- /dev/null +++ b/source/private/ResetColor.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..0c68bb1 --- /dev/null +++ b/source/private/TruncateString.ps1 @@ -0,0 +1,40 @@ +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] +} + +function TrimAnsi($string) { + $words = $string -split ' ' + $last = $words.length; + + while ($last > 0) { + if ((Measure-String $words[$last - 1]) -gt 0) { + break + } + + $last-- + } + + if ($last -ne $words.length) { + return $string + } + + ($words[0..$last] -join ' ') + ($words[$last..$words.length] -join '') +} \ 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..f78303d 100644 --- a/source/public/ConvertTo-NormalErrorView.ps1 +++ b/source/public/ConvertTo-NormalErrorView.ps1 @@ -11,64 +11,50 @@ 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 - } + $myinv = $InputObject.InvocationInfo + $posmsg = '' - if ( &{ Set-StrictMode -Version 1; $InputObject.PSMessageDetails } ) { - $posmsg = " : " + $InputObject.PSMessageDetails + $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 = " " + FirstLineIndent = "" } - } - $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 + + $line = $accentColor + "+ FullyQualifiedErrorId : " + $errorColor + $InputObject.FullyQualifiedErrorId | 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) - } + $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 + (GetErrorPrefix -InputObject $InputObject) + $InputObject.Exception.Message + $posmsg + $resetColor + } else { + $errorColor + (GetErrorPrefix -InputObject $InputObject) + $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..2bbb6d4 100644 --- a/source/public/Format-Error.ps1 +++ b/source/public/Format-Error.ps1 @@ -1,13 +1,13 @@ -function Format-Error { +filter 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 = "Count")] + [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,52 @@ 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", + $View = $global:ErrorView, - [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 + Set-StrictMode -Off + $ErrorActionPreference = 'Stop' + trap { 'Error found in error view definition: ' + $_.Exception.Message } + if ($InputObject.ErrorRecord) { + $InputObject = $InputObject.ErrorRecord } - process { - $InputObject + + $Views = @{ + ListImported = $true + ErrorAction = "Ignore" + ParameterName = "InputObject" } - end { - [bool]$ErrorViewRecurse = $Recurse - $ErrorView = $View + + if ($InputObject -is [System.Management.Automation.ErrorRecord]) { + if (($formatter = @(Get-Command "ConvertTo-$($View -replace "View$")ErrorView" @Views -ParameterType [System.Management.Automation.ErrorRecord]))) { + . ($formatter[0]) -InputObject $InputObject + } else { + ConvertTo-NormalErrorView $InputObject + } + } else { + if (($formatter = @(Get-Command "ConvertTo-$($View -replace "View$")ExceptionView" @Views -ParameterType [System.Exception]))) { + . ($formatter[0]) -InputObject $InputObject + } else { + ConvertTo-NormalExceptionView $InputObject + } } } diff --git a/source/public/Format-String.ps1 b/source/public/Format-String.ps1 new file mode 100644 index 0000000..1c4a6cb --- /dev/null +++ b/source/public/Format-String.ps1 @@ -0,0 +1,132 @@ + +filter Format-String { + <# + .SYNOPSIS + Formats a string to fit within a certain width, with optional indentation and alternating colors + .DESCRIPTION + Wraps a string on word breaks to fit within a certain width. + Preserves virtual terminal escape sequences, and supports indenting, + including different indents for the first line and others. + + Ignores ANSI escape sequences when measuring the length, + and handles wide characters (including treating emoji as 2 characters wide). + .LINK + Measure-String + #> + [Alias("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 + [Alias("HangingIndent")] + [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")" + $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 { + $output = [System.Text.StringBuilder]::new() + 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 $WordBoundaryRegex | Where-Object { $_.Length } | ForEach-Object { @{ Text = $_; Length = Measure-String $_ -EmojiAsWide } } + Write-Debug "$($line.Length) characters in line in $($slices.Count) words. $($AnsiRegex.Replace($line, ''))" + $lineLength = $IndentPadding.Length + foreach($slice in $slices) { + $lineLength = $lineLength + $slice.Length + Write-Verbose "+ $($slice.Length) = $lineLength <= $Width '$($slice.Text -replace "`e","``e")'" + if ($lineLength -le $Width) { + if ($lineLength -eq $slice.Length -and [string]::IsNullOrWhitespace($slice.Text)) { + Write-Debug "Skip whitespace '$($slice.Text)'" + $lineLength = $lineLength - $slice.Length + continue + } + $null = $buffer.Append($slice.Text) + } elseif ($slice.Length -gt $Width) { + Write-Debug "Slice too long $($slice.Length) > $Width" + $needLength = $Width - ($lineLength - $slice.Length) + + $remains = $slice + # If the slice is too long for a line, it's going to wrap anyway, so do it ourselves + while($remains.Length -gt $needLength) { + $next, $remains = $remains.Text -split "((?:(?:$AnsiPattern)*.(?:$AnsiPattern)*){1,$needLength})", 2 | Where-Object { $_.Length } | ForEach-Object { @{ Text = $_; Length = Measure-String $_ -EmojiAsWide } } + $null = $buffer.Append($next.Text).Append($newline).Append($WrappedIndent) + $lineLength = $WrappedIndent.Length + $needLength = $Width - $lineLength + } + + # Don't start a line with whitespace + if (![string]::IsNullOrWhitespace($remains.Text)) { + Write-Debug "Output $($lineLength) not whitespace '$($remains.Text)'" + $null = $buffer.Append($remains.Text) + $lineLength = $lineLength + $remains.Length + } + + } else { + Write-Verbose "Output $($lineLength - $slice.Length)" + $null = $buffer.Append($newline).Append($WrappedIndent) + $lineLength = $WrappedIndent.Length + # Don't start a line with whitespace + if (![string]::IsNullOrWhitespace($slice.Text)) { + Write-Debug "Output $($lineLength) not whitespace '$($slice.Text)'" + $null = $buffer.Append($slice.Text) + $lineLength = $WrappedIndent.Length + $slice.Length + } + } + } + if (!$FirstLine) { + $null = $output.Append($newline) + } + if ($PSBoundParameters.ContainsKey("IndentPadding")) { + $null = $output.Append($Colors[$color] + $IndentPadding + (<# TrimAnsi #> $buffer.ToString().TrimStart())) + } else { + $null = $output.Append($Colors[$color] + (<# TrimAnsi #> $buffer.ToString())) + } + $color = ($color + 1) % $Colors.Length + $null = $buffer.Clear() #.Append($Colors[$color]).Append($IndentPadding) + $lineLength = $IndentPadding.Length + $FirstLine = $false + $IndentPadding = $FirstLineIndent + } + $output.ToString() -replace "\s(?=$newline)" # trim trailing whitespace from each line + } +} \ No newline at end of file diff --git a/source/public/Measure-String.ps1 b/source/public/Measure-String.ps1 new file mode 100644 index 0000000..7677121 --- /dev/null +++ b/source/public/Measure-String.ps1 @@ -0,0 +1,101 @@ + +# https://github.com/sindresorhus/string-width + +# import stripAnsi from 'strip-ansi'; +# import {eastAsianWidth} from 'get-east-asian-width'; +# import emojiRegex from 'emoji-regex'; + +# const segmenter = new Intl.Segmenter(); + +# const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u; + +filter Measure-String { + <# + .SYNOPSIS + Measures the length of a string with support for escape sequences and wide characters + .DESCRIPTION + By default, ignores ANSI escape sequences when measuring the length + Optionally can treat ambiguous characters as wide + Optionally can treat emoji characters wide + .LINK + Measure-String + #> + param( + [ValidateNotNull()] + [Parameter(ValueFromPipeline)] + [string]$string, + + [switch]$AmbiguousAsWide, + + [switch]$EmojiAsWide, + + [switch]$countAnsiEscapeCodes + ) + if ($string.length -eq 0) { + return 0; + } + + if (!$countAnsiEscapeCodes) { + $string = $string -replace $AnsiRegex + } + + if ($string.length -eq 0) { + return 0; + } + + # PowerShell 5 (.NET 4) isn't UAX 29 compliant + $width = 0; + foreach ($character in $string.GetEnumerator()) { + $codePoint = [int]$character; + + # Ignore control characters + if ($codePoint -le 0x1F -or ($codePoint -ge 0x7F -and $codePoint -le 0x9F)) { + continue + } + + # Ignore zero-width characters + if (($codePoint -ge 0x200B -and $codePoint -le 0x200F) -or # Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark + $codePoint -eq 0xFEFF ) { + # Zero-width no-break space# Zero-width no-break space + continue + } + + # Ignore combining characters + if (($codePoint -ge 0x300 -and $codePoint -le 0x36F) -or # Combining diacritical marks + ($codePoint -ge 0x1AB0 -and $codePoint -le 0x1AFF) -or # Combining diacritical marks extended + ($codePoint -ge 0x1DC0 -and $codePoint -le 0x1DFF) -or # Combining diacritical marks supplement + ($codePoint -ge 0x20D0 -and $codePoint -le 0x20FF) -or # Combining diacritical marks for symbols + ($codePoint -ge 0xFE20 -and $codePoint -le 0xFE2F)) { + # Combining half marks + continue + } + + # Ignore surrogate pairs + if ($codePoint -ge 0xD800 -and $codePoint -le 0xDFFF) { + continue + } + + # Ignore variation selectors + if ($codePoint -ge 0xFE00 -and $codePoint -le 0xFE0F) { + continue + } + + # This covers some of the above cases, but we still keep them for performance reasons. + if ([Char]::GetUnicodeCategory($character) -in 'NonSpacingMark', 'SpacingCombiningMark', 'EnclosingMark', 'Format') { + continue + } + + if ($character -match $EmojiRegex) { + if ($EmojiAsWide) { + $width += 2 # Treat emojis as double width + } else { + $width += 1 # Treat emojis as single width + } + continue + } + + $width += Measure-EastAsianWidth $codePoint -AmbiguousAsWide:$AmbiguousAsWide + } + + return $width; +} \ 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 diff --git a/tests/Compatibility.Tests.ps1 b/tests/Compatibility.Tests.ps1 index e8c4118..4028dcd 100644 --- a/tests/Compatibility.Tests.ps1 +++ b/tests/Compatibility.Tests.ps1 @@ -1,62 +1,158 @@ #requires -Module Pansies -Describe "Format-Error produces the same results" { +Describe "Format-Error produces the same results for scriptblocks" { BeforeAll { - $_cacheErrorView = $global:ErrorView - try { - Invoke-Expression '$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd, Split-Path -Leaf $pwd' - } catch { } - $TestError = $Error[0] + # $Session = New-PSSession -EnableNetworkAccess + # Invoke-Command -Session $Session { $ErrorView = 'ConciseView' } + $ModuleVer = $GitVersion.$PSModuleName.MajorMinorPatch + $PSModuleManifestPath = Get-ChildItem $PSModuleOutputPath/$ModuleVer -Filter "$PSModuleName.psd1" -Recurse -ErrorAction Ignore - # Try to clear our ErrorView format data - Remove-Item "$PSScriptRoot/../output/ErrorView/ErrorView.backup" -ErrorAction Ignore - Rename-Item "$PSScriptRoot/../output/ErrorView/ErrorView.format.ps1xml" "ErrorView.backup" - Set-Content "$PSScriptRoot/../output/ErrorView/ErrorView.format.ps1xml" ( - '' + "`n" + - '') - Remove-Module ErrorView -ErrorAction SilentlyContinue - Update-FormatData + $NewConciseView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'ConciseView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) + $OldConciseView = [ScriptBlock]::Create( + "`$ErrorView = 'ConciseView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) - [System.Management.Automation.ErrorView]$global:ErrorView = 'ConciseView' - $ExpectedConciseView = $TestError | Out-String - Write-Host "$($PSStyle.Foreground.Red)ExpectedConciseView: " $PSStyle.Reset $ExpectedConciseView + $NewNormalView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'NormalView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) - [System.Management.Automation.ErrorView]$global:ErrorView = 'NormalView' - $ExpectedNormalView = $TestError | Out-String - Write-Host "$($PSStyle.Foreground.Red)ExpectedNormalView: " $PSStyle.Reset $ExpectedNormalView + $OldNormalView = [ScriptBlock]::Create( + "`$ErrorView = 'NormalView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) - [System.Management.Automation.ErrorView]$global:ErrorView = 'CategoryView' - $ExpectedCategoryView = $TestError | Out-String - Write-Host "$($PSStyle.Foreground.Red)ExpectedCategoryView: " $PSStyle.Reset $ExpectedCategoryView + $NewCategoryView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'CategoryView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) - [System.Management.Automation.ErrorView]$global:ErrorView = 'DetailedView' - $ExpectedDetailedView = $TestError | Out-String - # Write-Host "$($PSStyle.Foreground.Red)ExpectedDetailedView: " $PSStyle.Reset $ExpectedDetailedView + $OldCategoryView = [ScriptBlock]::Create( + "`$ErrorView = 'CategoryView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) + $NewDetailedView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'DetailedView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) - Remove-Item "$PSScriptRoot/../output/ErrorView/ErrorView.format.ps1xml" -ErrorAction Ignore - Rename-Item "$PSScriptRoot/../output/ErrorView/ErrorView.backup" "ErrorView.format.ps1xml" - Import-Module $PSScriptRoot/../output/ErrorView/ErrorView.psd1 -Force + $OldDetailedView = [ScriptBlock]::Create( + "`$ErrorView = 'DetailedView'`n" + + '.{ Invoke-Expression ''$R = "$([char]27)]8;;{0}`a{0}$([char]27)]8;;`a" -f $pwd,'' } 2>&1 | Out-String' + ) + } + It 'As the default CategoryView' { + $expectCV = pwsh -noprofile -c $OldConciseView + $actualCV = pwsh -noprofile -c $NewConciseView + $actualCV | Should -Be $expectCV } - AfterAll { - $global:ErrorView = $_cacheErrorView + It 'As the default NormalView' { + $expectNV = pwsh -noprofile -c $OldNormalView + $actualNV = pwsh -noprofile -c $NewNormalView + $actualNV | Should -Be $expectNV } - It 'As the default CategoryView' { - $actual = $TestError | Format-Error -View CategoryView | Out-String - $actual | Should -Be $ExpectedCategoryView + $expectCV = pwsh -noprofile -c $OldCategoryView + $actualCV = pwsh -noprofile -c $NewCategoryView + $actualCV | Should -Be $expectCV } - It 'As the default ConciseView' { - $actual = $TestError | Format-Error -View ConciseView | Out-String - $actual | Should -Be $ExpectedConciseView + It 'As the default DetailedView' { + $expectDV = pwsh -noprofile -c $OldDetailedView + $actualDV = pwsh -noprofile -c $NewDetailedView + + <# My DetailedView has the same information as the default, but without showing the ErrorRecord twice, so they aren't the same #> + $expectDV = @($expectDV -split "\W+" | Sort-Object -uniq) -notmatch "^Type|ErrorRecord|ParentContainsErrorRecordException" + # Also, I show the HResult in hex + $actualDV = @($actualDV -split "\W+" | Sort-Object -uniq) -notmatch "0x80131501" + # This test has failed once or twice, so I added this to help debug + if ($expectDV -ne $actualDV) { + $actualDV | Compare-Object $expectDV | Out-Host + } + $actualDV | Should -Be $expectDV + } + +} + +Describe "Format-Error produces the same results for scriptblocks" { + BeforeAll { + # $Session = New-PSSession -EnableNetworkAccess + # Invoke-Command -Session $Session { $ErrorView = 'ConciseView' } + $ModuleVer = $GitVersion.$PSModuleName.MajorMinorPatch + $PSModuleManifestPath = Get-ChildItem $PSModuleOutputPath/$ModuleVer -Filter "$PSModuleName.psd1" -Recurse -ErrorAction Ignore + + $NewConciseView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'ConciseView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + + $OldConciseView = [ScriptBlock]::Create( + "`$ErrorView = 'ConciseView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + + $NewNormalView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'NormalView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + + $OldNormalView = [ScriptBlock]::Create( + "`$ErrorView = 'NormalView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + + $NewCategoryView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'CategoryView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + + $OldCategoryView = [ScriptBlock]::Create( + "`$ErrorView = 'CategoryView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + $NewDetailedView = [ScriptBlock]::Create( + "Import-Module -Name $PSModuleManifestPath -ArgumentList 'DetailedView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + + $OldDetailedView = [ScriptBlock]::Create( + "`$ErrorView = 'DetailedView'`n" + + "$BuildRoot\tests\Write-AnError.ps1 2>&1 | Out-String" + ) + } + + It 'As the default CategoryView' { + $expectCV = pwsh -noprofile -c $OldConciseView + $actualCV = pwsh -noprofile -c $NewConciseView + $actualCV | Should -Be $expectCV } It 'As the default NormalView' { - $actual = $TestError | Format-Error -View NormalView | Out-String - $actual | Should -Be $ExpectedNormalView + $expectNV = pwsh -noprofile -c $OldNormalView + $actualNV = pwsh -noprofile -c $NewNormalView + $actualNV | Should -Be $expectNV + } + It 'As the default CategoryView' { + $expectCV = pwsh -noprofile -c $OldCategoryView + $actualCV = pwsh -noprofile -c $NewCategoryView + $actualCV | Should -Be $expectCV } It 'As the default DetailedView' { - $actual = $TestError | Format-Error -View DetailedView | Out-String - $actual | Should -Be $ExpectedDetailedView + $global:expectDV = pwsh -noprofile -c $OldDetailedView + $global:actualDV = pwsh -noprofile -c $NewDetailedView + + <# My DetailedView has the same information as the default, but without showing the ErrorRecord twice, so they aren't the same #> + $expectDV = @($expectDV -split "\W+" | Sort-Object -uniq) -notmatch "^Type" + # Also, I show the HResult in hex + $actualDV = @($actualDV -split "\W+" | Sort-Object -uniq) -notmatch "0x80131500" + # This test has failed once or twice, so I added this to help debug + if ($expectDV -ne $actualDV) { + $actualDV | Compare-Object $expectDV | Out-Host + } + $actualDV | Should -Be $expectDV } } \ No newline at end of file diff --git a/tests/WrapString.Tests.ps1 b/tests/WrapString.Tests.ps1 index 7959066..82efc17 100644 --- a/tests/WrapString.Tests.ps1 +++ b/tests/WrapString.Tests.ps1 @@ -2,16 +2,17 @@ Describe WrapString { BeforeAll { $CommandUnderTest = & (Get-Module ErrorView) { Get-Command WrapString } + $newline = [Environment]::Newline } It "Word-wraps text to keep it under a specified width" { "The quick brown fox jumped over the lazy dog and then ran away with the unicorn." | & $CommandUnderTest -Width 20 <# -Verbose #> | - Should -Be "The quick brown fox", "jumped over the lazy", "dog and then ran", "away with the", "unicorn." + Should -Be "The quick brown fox${newline}jumped over the lazy${newline}dog and then ran${newline}away with the${newline}unicorn." } It "Does not count ANSI escape sequences as characters" { - "The quick brown ${fg:red}fox${fg:clear} jumped over the lazy ${fg:green}dog and then ran away with the unicorn.${fg:clear}" | + "${bg:Gray20}The quick brown ${fg:red}fox${fg:clear} jumped over the lazy ${fg:green}dog and then ran away with the unicorn.${fg:clear}${bg:Clear}" | & $CommandUnderTest -Width 20 <# -Verbose #> | - Should -Be "The quick brown ${fg:red}fox${fg:clear}", "jumped over the lazy", "${fg:green}dog and then ran", "away with the", "unicorn.${fg:clear}" + Should -Be "${bg:Gray20}The quick brown ${fg:red}fox${fg:clear}${newline}jumped over the lazy${newline}${fg:green}dog and then ran${newline}away with the${newline}unicorn.${fg:clear}${bg:Clear}" } } diff --git a/tests/Write-AnError.ps1 b/tests/Write-AnError.ps1 new file mode 100644 index 0000000..f2eec8f --- /dev/null +++ b/tests/Write-AnError.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string]$Message = "We faked an error." +) + +function Write-AnError { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Message = "We faked an error." + ) + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new($Message), + "AnError", + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $null + ) + ) +} + +Write-AnError @PSBoundParameters