Implement Get-Error cmdlet as Experimental Feature (#10727)

This commit is contained in:
Steve Lee
2019-10-15 14:32:14 -07:00
committed by Aditya Patwardhan
parent 8b68a4cd99
commit d8d32d79c2
9 changed files with 478 additions and 36 deletions
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation;
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// Class for Get-Error implementation.
/// </summary>
[Experimental("Microsoft.PowerShell.Utility.PSGetError", ExperimentAction.Show)]
[Cmdlet(VerbsCommon.Get, "Error",
HelpUri = "https://docs.microsoft.com/powershell/module/microsoft.powershell.utility/get-error?view=powershell-7&WT.mc_id=ps-gethelp",
DefaultParameterSetName = NewestParameterSetName)]
public sealed class GetErrorCommand : PSCmdlet
{
internal const string ErrorParameterSetName = "Error";
internal const string NewestParameterSetName = "Newest";
internal const string AliasNewest = "Last";
/// <summary>
/// Gets or sets the error object to resolve.
/// </summary>
[Parameter(Position = 0, ValueFromPipeline = true, ParameterSetName = ErrorParameterSetName)]
[ValidateNotNullOrEmpty]
public PSObject InputObject { get; set; }
/// <summary>
/// Gets or sets the number of error objects to resolve starting with newest first.
/// </summary>
[Parameter(ParameterSetName = NewestParameterSetName)]
[Alias(AliasNewest)]
[ValidateRange(1, int.MaxValue)]
public int Newest { get; set; } = 1;
/// <summary>
/// Process the error object.
/// </summary>
protected override void ProcessRecord()
{
var errorRecords = new List<object>();
var index = 0;
if (InputObject != null)
{
if (InputObject.BaseObject is Exception || InputObject.BaseObject is ErrorRecord)
{
errorRecords.Add(InputObject);
}
}
else
{
var errorVariable = SessionState.PSVariable.Get("error");
var count = Newest;
ArrayList errors = (ArrayList)errorVariable.Value;
if (count > errors.Count)
{
count = errors.Count;
}
while (count > 0)
{
errorRecords.Add(errors[index]);
index++;
count--;
}
}
index = 0;
bool addErrorIdentifier = errorRecords.Count > 1 ? true : false;
foreach (object errorRecord in errorRecords)
{
PSObject obj = PSObject.AsPSObject(errorRecord);
obj.TypeNames.Insert(0, "PSExtendedError");
// Remove some types so they don't get rendered by those formats
obj.TypeNames.Remove("System.Management.Automation.ErrorRecord");
obj.TypeNames.Remove("System.Exception");
if (addErrorIdentifier)
{
obj.Properties.Add(new PSNoteProperty("PSErrorIndex", index++));
}
WriteObject(obj);
}
}
}
}
@@ -735,125 +735,184 @@ namespace Microsoft.PowerShell
_ui = ui;
}
public ConsoleColor FormatAccentColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{
return _ui.FormatAccentColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{
_ui.FormatAccentColor = value;
}
}
public ConsoleColor ErrorAccentColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.ErrorAccentColor; }
{
return _ui.ErrorAccentColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.ErrorAccentColor = value; }
{
_ui.ErrorAccentColor = value;
}
}
public ConsoleColor ErrorForegroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.ErrorForegroundColor; }
{
return _ui.ErrorForegroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.ErrorForegroundColor = value; }
{
_ui.ErrorForegroundColor = value;
}
}
public ConsoleColor ErrorBackgroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.ErrorBackgroundColor; }
{
return _ui.ErrorBackgroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.ErrorBackgroundColor = value; }
{
_ui.ErrorBackgroundColor = value;
}
}
public ConsoleColor WarningForegroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.WarningForegroundColor; }
{
return _ui.WarningForegroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.WarningForegroundColor = value; }
{
_ui.WarningForegroundColor = value;
}
}
public ConsoleColor WarningBackgroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.WarningBackgroundColor; }
{
return _ui.WarningBackgroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.WarningBackgroundColor = value; }
{
_ui.WarningBackgroundColor = value;
}
}
public ConsoleColor DebugForegroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.DebugForegroundColor; }
{
return _ui.DebugForegroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.DebugForegroundColor = value; }
{
_ui.DebugForegroundColor = value;
}
}
public ConsoleColor DebugBackgroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.DebugBackgroundColor; }
{
return _ui.DebugBackgroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.DebugBackgroundColor = value; }
{
_ui.DebugBackgroundColor = value;
}
}
public ConsoleColor VerboseForegroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.VerboseForegroundColor; }
{
return _ui.VerboseForegroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.VerboseForegroundColor = value; }
{
_ui.VerboseForegroundColor = value;
}
}
public ConsoleColor VerboseBackgroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.VerboseBackgroundColor; }
{
return _ui.VerboseBackgroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.VerboseBackgroundColor = value; }
{
_ui.VerboseBackgroundColor = value;
}
}
public ConsoleColor ProgressForegroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.ProgressForegroundColor; }
{
return _ui.ProgressForegroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.ProgressForegroundColor = value; }
{
_ui.ProgressForegroundColor = value;
}
}
public ConsoleColor ProgressBackgroundColor
{
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
get
{ return _ui.ProgressBackgroundColor; }
{
return _ui.ProgressBackgroundColor;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
set
{ _ui.ProgressBackgroundColor = value; }
{
_ui.ProgressBackgroundColor = value;
}
}
}
@@ -1355,6 +1355,9 @@ namespace Microsoft.PowerShell
}
}
// Format colors
public ConsoleColor FormatAccentColor { get; set; } = ConsoleColor.Green;
// Error colors
public ConsoleColor ErrorAccentColor { get; set; } = ConsoleColor.Cyan;
public ConsoleColor ErrorForegroundColor { get; set; } = ConsoleColor.Red;
@@ -25,7 +25,7 @@ CmdletsToExport = @(
'Get-TraceSource', 'Set-TraceSource', 'Add-Type', 'Get-TypeData', 'Remove-TypeData', 'Update-TypeData',
'Get-UICulture', 'Get-Unique', 'Get-Uptime', 'Clear-Variable', 'Get-Variable', 'New-Variable',
'Remove-Variable', 'Set-Variable', 'Get-Verb', 'Write-Verbose', 'Write-Warning', 'Invoke-WebRequest',
'Format-Wide', 'ConvertTo-Xml', 'Select-Xml'
'Format-Wide', 'ConvertTo-Xml', 'Select-Xml', 'Get-Error'
)
FunctionsToExport = @()
AliasesToExport = @('fhx')
@@ -38,6 +38,10 @@ PrivateData = @{
Name = 'Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace'
Description = 'Enables -BreakAll parameter on Debug-Runspace and Debug-Job cmdlets to allow users to decide if they want PowerShell to break immediately in the current location when they attach a debugger.'
}
@{
Name = 'Microsoft.PowerShell.Utility.PSGetError'
Description = 'Enable Get-Error cmdlet that displays detailed information about ErrorRecords included nested objects'
}
)
}
}
@@ -23,7 +23,7 @@ CmdletsToExport = @(
'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan', 'Get-TraceSource', 'Set-TraceSource',
'Add-Type', 'Get-TypeData', 'Remove-TypeData', 'Update-TypeData', 'Get-UICulture', 'Get-Unique', 'Get-Uptime',
'Clear-Variable', 'Get-Variable', 'New-Variable', 'Remove-Variable', 'Set-Variable', 'Get-Verb', 'Write-Verbose',
'Write-Warning', 'Invoke-WebRequest', 'Format-Wide', 'ConvertTo-Xml', 'Select-Xml'
'Write-Warning', 'Invoke-WebRequest', 'Format-Wide', 'ConvertTo-Xml', 'Select-Xml', 'Get-Error'
)
FunctionsToExport = @()
AliasesToExport = @('fhx')
@@ -36,6 +36,10 @@ PrivateData = @{
Name = 'Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace'
Description = 'Enables -BreakAll parameter on Debug-Runspace and Debug-Job cmdlets to allow users to decide if they want PowerShell to break immediately in the current location when they attach a debugger.'
}
@{
Name = 'Microsoft.PowerShell.Utility.PSGetError'
Description = 'Enable Get-Error cmdlet that displays detailed information about ErrorRecords included nested objects'
}
)
}
}
@@ -137,6 +137,10 @@ namespace System.Management.Automation.Runspaces
"System.Management.Automation.ScriptBlock",
ViewsOf_System_Management_Automation_ScriptBlock());
yield return new ExtendedTypeDefinition(
"PSExtendedError",
ViewsOf_System_Management_Automation_GetError());
yield return new ExtendedTypeDefinition(
"System.Management.Automation.ErrorRecord",
ViewsOf_System_Management_Automation_ErrorRecord());
@@ -722,6 +726,197 @@ namespace System.Management.Automation.Runspaces
.EndControl());
}
// This generates a custom view for ErrorRecords and Exceptions making
// specific nested types defined in $expandTypes visible. It also handles
// IEnumerable types. Nested types are indented by 4 spaces.
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Automation_GetError()
{
yield return new FormatViewDefinition("GetErrorInstance",
CustomControl.Create()
.GroupByProperty("PSErrorIndex", label: "ErrorIdentifier")
.StartEntry()
.AddScriptBlockExpressionBinding(@"
$maxDepth = 10
$ellipsis = ""`u{2026}""
$resetColor = ''
if ($Host.UI.SupportsVirtualTerminal) {
$resetColor = ""`e[0m""
}
function Get-VT100Color([string] $color) {
if (! $Host.UI.SupportsVirtualTerminal) {
return ''
}
$colors = @{
'Black' = ""`e[2;30m""
'DarkRed' = ""`e[2;31m""
'DarkGreen' = ""`e[2;32m""
'DarkYellow' = ""`e[2;33m""
'DarkBlue' = ""`e[2;34m""
'DarkMagenta' = ""`e[2;35m""
'DarkCyan' = ""`e[2;36m""
'Gray' = ""`e[2;37m""
'DarkGray' = ""`e[1;30m""
'Red' = ""`e[1;31m""
'Green' = ""`e[1;32m""
'Yellow' = ""`e[1;33m""
'Blue' = ""`e[1;34m""
'Magenta' = ""`e[1;35m""
'Cyan' = ""`e[1;36m""
'White' = ""`e[1;37m""
}
return $colors[$color]
}
function Show-ErrorRecord($obj, [int]$indent = 0, [int]$depth = 1) {
$newline = [Environment]::Newline
$output = [System.Text.StringBuilder]::new()
$prefix = ' ' * $indent
$accentColor = Get-VT100Color $Host.PrivateData.FormatAccentColor
$expandTypes = @(
'Microsoft.Rest.HttpRequestMessageWrapper'
'Microsoft.Rest.HttpResponseMessageWrapper'
'System.Management.Automation.InvocationInfo'
)
# first find the longest property so we can indent properly
$propLength = 0
foreach ($prop in $obj.PSObject.Properties) {
if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Name.Length -gt $propLength) {
$propLength = $prop.Name.Length
}
}
$addedProperty = $false
foreach ($prop in $obj.PSObject.Properties) {
# don't show empty properties or our added property for $error[index]
if ($prop.Value -ne $null -and $prop.Value -ne [string]::Empty -and $prop.Value.count -gt 0 -and $prop.Name -ne 'PSErrorIndex') {
$addedProperty = $true
$null = $output.Append($prefix)
$null = $output.Append($accentColor)
$null = $output.Append($prop.Name)
$propNameIndent = ' ' * ($propLength - $prop.Name.Length)
$null = $output.Append($propNameIndent)
$null = $output.Append(' : ')
$null = $output.Append($resetColor)
$newIndent = $indent + 4
# only show nested objects that are Exceptions, ErrorRecords, or types defined in $expandTypes and types not in $ignoreTypes
if ($prop.Value -is [Exception] -or $prop.Value -is [System.Management.Automation.ErrorRecord] -or
$expandTypes -contains $prop.TypeNameOfValue -or ($prop.TypeNames -ne $null -and $expandTypes -contains $prop.TypeNames[0])) {
if ($depth -ge $maxDepth) {
$null = $output.Append($ellipsis)
}
else {
$null = $output.Append($newline)
$null = $output.Append((Show-ErrorRecord $prop.Value $newIndent ($depth + 1)))
}
}
# `TargetSite` has many members that are not useful visually, so we have a reduced view of the relevant members
elseif ($prop.Name -eq 'TargetSite' -and $prop.Value.GetType().Name -eq 'RuntimeMethodInfo') {
if ($depth -ge $maxDepth) {
$null = $output.Append($ellipsis)
}
else {
$targetSite = [PSCustomObject]@{
Name = $prop.Value.Name
DeclaringType = $prop.Value.DeclaringType
MemberType = $prop.Value.MemberType
Module = $prop.Value.Module
}
$null = $output.Append($newline)
$null = $output.Append((Show-ErrorRecord $targetSite $newIndent ($depth + 1)))
}
}
# `StackTrace` is handled specifically because the lines are typically long but necessary so they are left justified without additional indentation
elseif ($prop.Name -eq 'StackTrace') {
# for a stacktrace which is usually quite wide with info, we left justify it
$null = $output.Append($newline)
$null = $output.Append($prop.Value)
}
# Dictionary and Hashtable we want to show as Key/Value pairs, we don't do the extra whitespace alignment here
elseif ($prop.Value.GetType().Name.StartsWith('Dictionary') -or $prop.Value.GetType().Name -eq 'Hashtable') {
$isFirstElement = $true
foreach ($key in $prop.Value.Keys) {
if ($isFirstElement) {
$null = $output.Append($newline)
}
if ($key -eq 'Authorization') {
$null = $output.Append(""${prefix} ${accentColor}${key} : ${resetColor}${ellipsis}${newline}"")
}
else {
$null = $output.Append(""${prefix} ${accentColor}${key} : ${resetColor}$($prop.Value[$key])${newline}"")
}
$isFirstElement = $false
}
}
# if the object implements IEnumerable and not a string, we try to show each object
# We ignore the `Data` property as it can contain lots of type information by the interpreter that isn't useful here
elseif (!($prop.Value -is [System.String]) -and $prop.Value.GetType().GetInterface('IEnumerable') -ne $null -and $prop.Name -ne 'Data') {
if ($depth -ge $maxDepth) {
$null = $output.Append($ellipsis)
}
else {
$isFirstElement = $true
foreach ($value in $prop.Value) {
$null = $output.Append($newline)
if (!$isFirstElement) {
$null = $output.Append($newline)
}
$null = $output.Append((Show-ErrorRecord $value $newIndent ($depth + 1)))
$isFirstElement = $false
}
}
}
# anything else, we use ToString()
else {
$value = $prop.Value.ToString().Trim()
$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()
}
Show-ErrorRecord $_
")
.EndEntry()
.EndControl());
}
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Automation_ErrorRecord()
{
yield return new FormatViewDefinition("ErrorInstance",
@@ -837,18 +1032,6 @@ namespace System.Management.Automation.Runspaces
}
return ($string.Substring(0,$length) -split '\s',-2)[0]
#if (-not $string.Contains(' ')) {
# return $string.Substring(0, $length)
#}
#$split = $string.Substring(0, $length).Split(' ')
#if ($split.Count -gt 1) {
# return [string]::Join(' ', $split, 0, $split.Count - 1)
#}
#else {
# return $split[0]
#}
}
$errorColor = ''
@@ -4557,6 +4557,7 @@ end {
new SessionStateAliasEntry("rp", "Remove-ItemProperty", string.Empty, ReadOnly),
new SessionStateAliasEntry("rmo", "Remove-Module", string.Empty, ReadOnly),
new SessionStateAliasEntry("rv", "Remove-Variable", string.Empty, ReadOnly),
new SessionStateAliasEntry("gerr", "Get-Error", string.Empty, ReadOnly),
new SessionStateAliasEntry("rvpa", "Resolve-Path", string.Empty, ReadOnly),
new SessionStateAliasEntry("sal", "Set-Alias", string.Empty, ReadOnly),
new SessionStateAliasEntry("sbp", "Set-PSBreakpoint", string.Empty, ReadOnly),
@@ -0,0 +1,93 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Describe 'Get-Error tests' -Tag CI {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('Microsoft.PowerShell.Utility.PSGetError')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'Microsoft.PowerShell.Utility.PSGetError' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
It 'Get-Error resolves $Error[0] and includes InnerException' {
try {
1/0
}
catch {
}
$out = Get-Error | Out-String
$out | Should -BeLikeExactly '*InnerException*'
}
It 'Get-Error -Newest `<count>` works: <scenario>' -TestCases @(
@{ scenario = 'less than total'; count = 1; paramname = 'Newest' }
@{ scenario = 'equal to total'; count = 2; paramname = 'Last' }
@{ scenario = 'greater than total'; count = 9999; paramname = 'Newest' }
){
param ($count, $paramname)
try {
1/0
}
catch {
}
try {
get-item (new-guid) -ErrorAction SilentlyContinue
}
catch {
}
$params = @{ $paramname = $count }
$out = Get-Error @params
$expected = $count
if ($count -eq 9999) {
$expected = $error.Count
}
$out.Count | Should -Be $expected
}
It 'Get-Error -Newest with invalid value `<value>` should fail' -TestCases @(
@{ value = 0 }
@{ value = -2 }
){
param($value)
{ Get-Error -Newest $value } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.GetErrorCommand'
}
It 'Get-Error will accept pipeline input' {
try {
1/0
}
catch {
}
$out = $error[0] | Get-Error | Out-String
$out | Should -BeLikeExactly '*-2146233087*'
}
It 'Get-Error will handle Exceptions' {
try {
Invoke-Expression '1/d'
}
catch {
}
$out = Get-Error | Out-String
$out | Should -BeLikeExactly '*ExpectedValueExpression*'
$out | Should -BeLikeExactly '*UnexpectedToken*'
}
}
@@ -73,6 +73,7 @@ Describe "Verify approved aliases list" -Tags "CI" {
"Alias", "gcm", "Get-Command", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", ""
"Alias", "gcs", "Get-PSCallStack", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", ""
"Alias", "gdr", "Get-PSDrive", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", ""
"Alias", "gerr", "Get-Error", $( $CoreWindows -or $CoreUnix), "ReadOnly", "", ""
"Alias", "ghy", "Get-History", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", ""
"Alias", "gi", "Get-Item", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", ""
"Alias", "gin", "Get-ComputerInfo", $($FullCLR -or $CoreWindows ), "", "", ""
@@ -269,6 +270,7 @@ Describe "Verify approved aliases list" -Tags "CI" {
"Cmdlet", "Get-Credential", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "Get-Culture", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "Get-Date", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "Get-Error", "", $( $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "Get-Event", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "Get-EventLog", "", $($FullCLR ), "", "", ""
"Cmdlet", "Get-EventSubscriber", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"