Sometimes a PowerShell function needs to call itself to continue processing. Although we should avoid creating recursive functions (if possible) due to their increased complexity, it's necessary on some occasions.

One example I can think of is enumerating Active Directory (AD) group members. AD groups can contain users and other groups as well. Those groups can then contain other groups that contain other groups…you get the point.

PowerShell should call the original function that enumerates the groups for each one of those child groups. A typical example of a recursive function looks something like this:

function Get-ADGroupMembersRecurse {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$GroupName
    )
    foreach ($member in (Get-AdGroupMember -Identity $GroupName)) {
        if ($member.objectClass -eq 'group') {
            Get-AdGroupMembersRecurse -GroupName $member.Name
        } else {
            $member
        }
    }
}

This function will query the membership of each group, and if it finds a group inside, it will then call the original function to enumerate members of that group as well.

What happens though when you need to perform a step only when calling the Get-AdGroupMembersRecurse function by itself? When calling this function externally, we want to perform something completely different. In this case, we need to use the Get-PSCallStack cmdlet.

The Get-PSCallStack command allows you to view the call stack or all commands executed in the current execution run. With Get-PSCallStack you can see a history of all commands run as part of a single execution step.

For example, put two functions inside of a script, have one call the other, and add Get-PSCallStack to the called function.

function Get-ThingInFile {
    param(
        [Parameter()]
        [string]$FilePath
    )
    if (Test-Path -Path $filePath) {
        Set-ThingOnFile -FilePath $FilePath
    } else {
        Write-Host "$filePath does not exist."
    }
}
function Set-ThingOnFile {
    param(
        [Parameter()]
        [string]$FilePath
    )
    Get-PSCallStack
}

When you run Get-ThingInFile, you'll see that the call stack knows you originally called Get-ThingInFile, which then called Set-ThingOnFile. The call stack is the history of all functions run, and Get-PSCallStack is the command to see the call stack.

PS> Get-ThingInFile -FilePath '.\file.txt'
Displaying the callstack

Displaying the callstack

Get-PSCallStack returns the call stack as an array of commands in the order of their execution. In this example, the Set-ThingOnFile command reference would be index 0 followed by Get-ThingInFile, which would be index 1.

If a function references Get-PSCallStack, the function that called that function will always be index 1. This represents the last function run before the current one. This is why you see Get-ThingInFile as the second element displayed above.

Let's use this ability in a recursive function to perform only some action if the function called itself. We can set some conditional logic to check for the function that called the current one.

In our AD group example, I'll add this logic simply to write a message to the console if the Get-AdGroupMembersRecurse function called itself.

function Get-ADGroupMembersRecurse {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$GroupName
    )
    if ((Get-PSCallStack)[1].Command -eq 'Get-ADGroupMembersRecurse') {
        Write-Host "The function called itself."
    } else {
        foreach ($member in (Get-AdGroupMember -Identity $GroupName)) {
            if ($member.objectClass -eq 'group') {
                Get-AdGroupMembersRecurse -GroupName $member.Name
            } else {
                $member
            }
        }
    }
}

I'll create a test group with the Domain Admins group inside of it as a test.

Subscribe to 4sysops newsletter!

PS> New-AdGroup -Name 'My Group'
PS> Add-AdGroupMember -Identity 'My Group' -Members 'Domain Admins'

When Get-AdGroupMembersRecurse runs, you'll see that it just returns "The function called itself." This is because we caught it. Otherwise, it would have enumerated the group members as expected.

1 Comment
  1. Martin 8 months ago

    Hi,

    In your functions “Get-ADGroupMembersRecurse” you’r using $member.name to call the function again. Sometimes the name attribute is not renamed together with other “Name” attributes in AD. In this case the function will get errors. A better solution is to use “$member.distinguishedname”!

    Regards Martin

Leave a reply

Your email address will not be published.

*

© 4sysops 2006 - 2022

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account