Weekend Scripter: A Hidden Gem in the PowerShell Ocean: Get-PSCallStack

Doctor Scripto

Summary: Microsoft PowerShell MVP, Jeff Wouters, talks about using the Get-PSCallStack Windows PowerShell cmdlet.

Microsoft Scripting Guy, Ed Wilson, is here. Today Windows PowerShell MVP, Jeff Wouters, is back with another guest blog post. To read more of Jeff's previous guest posts, see these Hey, Scripting Guy! Blog posts.

And now, Jeff…

I once tweeted the following:

Image of quote

This blog post is all about that quote.

Some time ago, I found myself in a situation where I needed to create a generic function that knew what other function or script called it. Let me explain…

I wanted to write a function to log information to the screen, to a SQL Server database and/or to a file.
So if function one calls the logging function, the logging function needed to figure that it was called by function one.

After searching and failing because I was overthinking it, I finally gave up and asked for help from my fellow Windows PowerShell MVPs.

As always, I got a reply. In this case, the reply was from Oisin Grehan, with exactly the answer I was looking for: Get-PSCallStack.

The Get-PSCallStack cmdlet gets the current call stack. It was initially written to be used with the Windows PowerShell debugger. However, it can be used outside the debugger to get the call stack inside a script or function. This is exactly what the doctor ordered!

There are many places you can get a call stack, so let’s go through them one-by-one.

A script

When called from a script, the default output only provides you with the properties Command, Arguments, and Location. However, there are more properties hidden that you can show by using the Select-Object cmdlet:

Image of command output

So now let’s turn that into a reusable function and look at the call stack again:

Image of command output

That’s too much information! I only want the information from the command in the script, and not the function itself.

If the call stack only has a single entry, I want that entry. But if there are two (or more), I want the last entry, right?

function Get-Execution {

    $CallStack = Get-PSCallStack | Select-Object -Property *

    if ($CallStack.Count -eq 1) {

        $CallStack[0]

    } else {

        $CallStack[($CallStack.Count – 1)]

    }

}

Note  Remember that an array starts counting at 0, so selecting the first object in an array would be $VariableName[0].

Calling from a function in a script

The previous code will work when calling the Get-Execution function directly from the script. But what if you, like me, prefer to write functions—so you’ll have a function that calls Get-Execution. You’ll want the entry in the call stack for that function. Let’s see what happens:

Image of command output

It works! You can trust me when I write that it keeps working—no matter how large the tree of functions calling functions—it keeps working.

But wait, there’s more!

Get-Execution in a module

Reusable functions are nice, but having to copy them into all my scripts is not something I would prefer. So what if you place Get-Execution in a module—let’s say a module with a lot of useful functions that you use in many of your scripts?
This would negate the need to copy the code into all your scripts. Simply load the module, and all the functions in that mode are available to you. Let’s see if it still works:

Image of command output

Yes, it still works!

Now let’s try a function in the script calling Get-Execution that’s inside the module:

Image of command output

Yup, still works like a charm.

When will it not work?

This function will not work when calling from a command prompt or in the Windows PowerShell ISE from an unsaved file or selection. In those situations, no call stack is available.

In the case of the command prompt, I’ve found a difference in the behavior in Windows PowerShell 2.0 versus the later versions.

In Windows PowerShell 2.0, no output was given if no call stack was found. Additionally, no error was given. So I was able to check the variable to have no value by comparing to $null. In The later versions of Windows PowerShell, I found that this wasn’t an option because Get-PSCallStack provided output when no call stack was found:

Image of command output

Let’s check for an empty call stack, and if no call stack is found, we'll provide an error message:

That would mean we’re done, right?

Wrong…

When is the default data not enough?

I am actually missing one property in the output (although the data itself is available): The location of the script!

The current output provides the path, but only combined with the name of the script. I want a separate property with the path of the script. Here’s the code that also outputs the location of the script by taking it from ScriptName:

function Get-Execution {

    $CallStack = Get-PSCallStack | Select-Object -Property *

    if (

         ($CallStack.Count -ne $null) -or

         (($CallStack.Command -ne '<ScriptBlock>') -and

         ($CallStack.Location -ne '<No file>') -and

         ($CallStack.ScriptName -ne $Null))

    ) {

        if ($CallStack.Count -eq 1) {

            $Output = $CallStack[0]

            $Output | Add-Member -MemberType NoteProperty -Name ScriptLocation -Value $((Split-Path $_.ScriptName)[0]) -PassThru

        } else {

            $Output = $CallStack[($CallStack.Count – 1)]

            $Output | Add-Member -MemberType NoteProperty -Name ScriptLocation -Value $((Split-Path $Output.ScriptName)[0]) -PassThru

        }

    } else {

        Write-Error -Message 'No callstack detected' -Category 'InvalidData'

    }

}

Use case?

Why would I want to write such a function?

I like to log the actions of my scripts to a file or to a database—maybe even to the event log. So when I deliver a script to a customer, I make use of this Get-Execution function to mention which file or script did something. Let’s take an example…

I’ve got a Write-Log function, which combined with Get-Execution, can be used like this:

$Execution = Get-Execution

Write-Log -Database 'Logging' `

-DBServer 'SQLDB01' `

–Status ‘Error’ `

-Time ((Get-Date -format o) -replace ':','.') `

-Message "$($Execution.Command) at $($Execution.Location)"

~Jeff

Thanks for sharing, Jeff.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

0 comments

Discussion is closed.

Feedback usabilla icon