Freigeben über


getting a stack trace in PowerShell

One of the most annoying "features" of PowerShell is that when the script crashes, it prints no stack trace, so finding the cause of the error is quite difficult. The exception object System.Management.Automation.ErrorRecord actually has the property ScriptStackTrace that contains the trace, it's just that the trace doesn't get printed on error. You can wrap your code into your own try/catch and print the trace. Or you can define a different default formatting for this class, and get the stack trace printed by default.

How to change the default formatting. First I'll tell how it was done, and then will show the whole contents.

If you want to start from scratch, open $PSHOME\PowerShellCore.format.ps1xml and copy the definition of formatting for the type System.Management.Automation.ErrorRecord to a your own separate file Format.ps1xml. After the last entry <ExpressionBinding>, add your own:

                             <ExpressionBinding>
                                <ScriptBlock>
                                    $_.ScriptStackTrace
                                </ScriptBlock>
                            </ExpressionBinding>

That's basically it. Well, plus a minor fix: the default implementation doesn't always include the LF at the end of the message, and if it doesn't, the stack trace ends up stuck directly to the end of the last line. To fix it, add the "`n" in the previous clause:

                                         elseif (! $_.ErrorDetails -or ! $_.ErrorDetails.Message) {
                                            $_.Exception.Message + $posmsg + "`n"  # SB-changed
                                        } else {
                                            $_.ErrorDetails.Message + $posmsg + "`n" # SB-changed
                                        }

After you have your Format.ps1xml ready, import it from your script:

 $spath = Split-Path -parent $PSCommandPath
Update-FormatData -PrependPath "$spath\Format.ps1xml"

Once imported, it will affect the whole PowerShell example.  Personally I also import it in ~\Documents\WindowsPowerShell\profile.ps1, so that at least on my machine I get the messages with the stack trace from all the normal running of PowerShell.

A weird thing is that if I do

 Get-FormatData -TypeName System.Management.Automation.ErrorRecord

I get nothing. But it works. I guess some special magic is associated with this class.

Also, if you want to get the stack traces from the remote sessions, you've got to copy the format file to that machine and load it in the session. The conversion of the objects to strings is done on the remote side, so the formatting has to be loaded there too.

And now for convenience the whole format file. The comment in $PSHOME\PowerShellCore.format.ps1xml says that it's the sample code, so it's got to be fine to use as another sample:

 <?xml version="1.0" encoding="utf-8" ?>
<!-- *******************************************************************


These sample files contain formatting information used by the Windows 
PowerShell engine. Do not edit or change the contents of this file 
directly. Please see the Windows PowerShell documentation or type 
Get-Help Update-FormatData for more information.

Copyright (c) Microsoft Corporation.  All rights reserved.
 
THIS SAMPLE CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY 
OF ANY KIND,WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
PURPOSE. IF THIS CODE AND INFORMATION IS MODIFIED, THE ENTIRE RISK OF USE
OR RESULTS IN CONNECTION WITH THE USE OF THIS CODE AND INFORMATION 
REMAINS WITH THE USER.

 
******************************************************************** -->
 
<Configuration>
  
  <ViewDefinitions>
        <View>
            <Name>ErrorInstance</Name>
            <OutOfBand />
            <ViewSelectedBy>
                <TypeName>System.Management.Automation.ErrorRecord</TypeName>
            </ViewSelectedBy>
            <CustomControl>
                <CustomEntries>
                    <CustomEntry>
                       <CustomItem>
                            <ExpressionBinding>
                                <ScriptBlock>
                                    if ($_.FullyQualifiedErrorId -ne "NativeCommandErrorMessage" -and $ErrorView -ne "CategoryView")
                                    {
                                        $myinv = $_.InvocationInfo
                                        if ($myinv -and $myinv.MyCommand)
                                        {
                                            switch -regex ( $myinv.MyCommand.CommandType )
                                            {
                                                ([System.Management.Automation.CommandTypes]::ExternalScript)
                                                {
                                                    if ($myinv.MyCommand.Path)
                                                    {
                                                        $myinv.MyCommand.Path + " : "
                                                    }
                                                    break
                                                }
                                                ([System.Management.Automation.CommandTypes]::Script)
                                                {
                                                    if ($myinv.MyCommand.ScriptBlock)
                                                    {
                                                        $myinv.MyCommand.ScriptBlock.ToString() + " : "
                                                    }
                                                    break
                                                }
                                                default
                                                {
                                                    if ($myinv.InvocationName -match '^[&amp;\.]?$')
                                                    {
                                                        if ($myinv.MyCommand.Name)
                                                        {
                                                            $myinv.MyCommand.Name + " : "
                                                        }
                                                    }
                                                    else
                                                    {
                                                        $myinv.InvocationName + " : "
                                                    }
                                                    break
                                                }
                                            }
                                        }
                                        elseif ($myinv -and $myinv.InvocationName)
                                        {
                                            $myinv.InvocationName + " : "
                                        }
                                    }
                                </ScriptBlock>
                            </ExpressionBinding>
                            <ExpressionBinding>
                                <ScriptBlock>
                                   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 ( &amp; { Set-StrictMode -Version 1; $_.PSMessageDetails } ) {
                                            $posmsg = " : " +  $_.PSMessageDetails + $posmsg 
                                        }

                                        $indent = 4
                                        $width = $host.UI.RawUI.BufferSize.Width - $indent - 2

                                        $errorCategoryMsg = &amp; { 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 = &amp; { 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"  # SB-changed
                                        } else {
                                            $_.ErrorDetails.Message + $posmsg + "`n" # SB-changed
                                        }
                                   }
                                </ScriptBlock>
                            </ExpressionBinding>
                            <ExpressionBinding>
                                <ScriptBlock>
                                    $_.ScriptStackTrace
                                </ScriptBlock>
                            </ExpressionBinding>
                        </CustomItem>
                    </CustomEntry>
                </CustomEntries>
            </CustomControl>
        </View>
    </ViewDefinitions>
</Configuration>