powershellcallstackpowershell-5.1

Get-PSCallStack in Windows PowerShell


I am writing a recursive function and would like to add a debugging mode with some (function) caller information along with the ScriptLineNumber.
In PowerShell 7, I am able to do something like:

MyScript.ps1

[CmdletBinding()]param()
function MyFunction($Depth = 0) {
    if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') {
        $Caller = (Get-PSCallStack)[1]
        Write-Debug "Caller: $($Caller.FunctionName), Line: $($Caller.ScriptLineNumber)"
    }
    Write-Host 'Current depth:' $Depth
    if ($Depth -lt 1) { MyFunction ($Depth + 1) }
}

MyFunction   
.\MyScript.ps1 -Debug
DEBUG: Caller: <ScriptBlock>, Line: 11
Current depth: 0
DEBUG: Caller: MyFunction, Line: 8
Current depth: 1

How can I do this in Windows PowerShell 5.1?

I am lacking the C# knowledge, but I guess the answer is hidden in this C# code but I have no clue where Context.Debugger.GetCallStack() comes from...


Solution

  • Before answering I should point out that Get-PSCallStack is available in Windows PowerShell 5.1, and hasn't changed since - so this exercise is likely entirely futile.


    I am lacking the C# knowledge, but I guess the answer is hidden in this C# code but I have no clue where Context.Debugger.GetCallStack() comes from...

    Context is a protected member of PSCmdlet (or rather, its ancestor class InternalCommand), which is why you can invoke it from an instance method declared by a derived type (like the GetPSCallStackCommand class).

    Since we don't have access to non-public member from inside a PSScriptCmdlet, we'll need a bit of reflection magic:

    function Get-PSScriptCallStack {
        [CmdletBinding()]
        param() 
    
        $nonPublicInstanceFlags = [System.Reflection.BindingFlags]'Instance,NonPublic'
        
        $member_psCmdletContext = $PSCmdlet.GetType().GetMembers($nonPublicInstanceFlags).Where({ $_.Name -eq 'Context' })[0]
        $contextInstance = $member_psCmdletContext.GetValue($PSCmdlet, @())
        
        $member_contextDebugger = $contextInstance.GetType().GetMembers($nonPublicInstanceFlags).Where({ $_.Name -eq 'Debugger' })[0]
        $debuggerInstance = $member_contextDebugger.GetValue($contextInstance, @())
    
        # Debugger.GetCallStack() is public
        $debuggerInstance.GetCallStack()
    }