Tag Archives: recursive function

Background Jobs and Recursive Functions

I recently had a thought: I have not written much on background jobs over the last nearly eight years. I should do more of that. Part of it stemmed from recently preparing an old post that was published elsewhere to be brought back to life here on tommymaynard.com. That one was Keeping a Continuous Total.

In that post, I wrote, “Next up is likely putting this code into a background job; it’s not the quickest thing I’ve written (although I blame that on the speed of the PowerShell Gallery lookup process, perhaps). Maybe a background job that runs in, or starts at, the end of the profile script. This, in order that these slow-to-obtain results are available sooner and with minimal impact.”

Well, I did that. While the function to do lookups in the PowerShell Gallery already existed in my profile script, I added a new function to my profile script that includes various background job commands that is invoked by my profile script. And, it turns out I was able to make it a recursive function, as well. You never think you will need one of those until the opportunity presents itself. I am so glad I noticed this opportunity.

Let’s start with my Show-PSGalleryProject function. You are welcome to use this as well. This function goes out to the PowerShell Gallery and determines the download count of each of my scripts and modules published there. This function takes some time to run, therefore, I want it to run in the background, so the results are available quicker than they would be otherwise.

Function Show-PSGalleryProject {
    [CmdletBinding()]
    Param (
        [System.Array]$Projects = ('TMOutput','Start-1to100Game3.0','Get-TMVerbSynonym',
            'SinkProfile','Show-PSDriveMenu','Switch-Prompt')
    )

    Foreach ($Project in $Projects) {
        If (Find-Module -Name $Project -ErrorAction SilentlyContinue) {
            $TempVar = Find-Module -Name $Project; $Type = 'Module'
        } ElseIf (Find-Script -Name $Project) {
            $TempVar = Find-Script -Name $Project; $Type = 'Script'
        }
        $TotalDownloads = [int]$TotalDownloads + [int]$TempVar.AdditionalMetadata.downloadCount
        [PSCustomObject]@{
            Name = $TempVar.Name
            Type = $Type
            Version = $TempVar.Version
            Downloads = $TempVar.AdditionalMetadata.downloadCount
            TotalDownloads = $TotalDownloads
        }
    } # End Foreach.
} # End Function: Show-PSGalleryProject.

The invocation of the above function is controlled by the below function. So, the Show-PSGalleryProject function goes out to the gallery to collect information, and the Show-PSGalleryProjectJob function orchestrates this process. Let me explain what this section function does. But first, take a look at it and see what you can extract yourself.

Set-Alias -Name psgal-Value Show-PSGalleryProjectJob
Function Show-PSGalleryProjectJob {
	$FunctionName = $MyInvocation.MyCommand.Name
	if (-Not(Get-Job -Name $FunctionName -ErrorAction SilentlyContinue)) {
		Start-Job -Name $FunctionName -ScriptBlock ${Function:Show-PSGalleryProject} | Out-Null
	} else {
		if ((Get-Job -Name $FunctionName).State -eq 'Completed') {
			$JobEndTime = (Get-Job -Name $FunctionName).PSEndTime
			Receive-Job -Name $FunctionName |
				Select-Object -Property *,@{Name='EndTime'; Expression={$JobEndTime}} -ExcludeProperty RunspaceId |
				Format-Table -AutoSize
			Remove-Job -Name $FunctionName
			& $FunctionName
		} else {
			Write-Warning -Message "Please wait. The $FunctionName background job is $((Get-Job -Name Show-PSGalleryProjectJob).State.ToLower()) (Id: $((Get-Job -Name Show-PSGalleryProjectJob).Id))."
		}
	}
}
psgal

Buckle up; here we go.

Line 1: Create the psgal alias for the function.
Line 2 (and 18): Define the Show-PSGalleryProjectJob function.
Line 3: Create the $FunctionName variable and assign it the name of this function using the $MyInvocation variable. The function’s name is used repeatedly throughout the function, so it made sense to store it in a variable.
Line 4: Include an if-else language construct. The if portion determines if there is a background job called Show-PSGalleryProjectJobor not.
Line 5: If that background job does not exist, it should be created and started. The Start-Job cmdlet’s ScriptBlock property invokes the Show-PSGalleryProject function.
Line 6: This is the beginning of else portion. The lines beneath it will run if there is already a background job running called Show-PSGalleryProjectJob.
Line 7 – 13: Nested in the else portion is another if-else construct. If the job is complete we run the commands in the if portion. If the job is not yet complete, we run the commands in the else portion below. The if portion does all of the following: it collects the end time of the job and stores it in $JobEndTime, it uses Receive-Job to collect the results of the completed background job, it pipes those results to Select-Object and displays all the default properties, as well as the end time we add using a calculated property. It takes those results and pipes them to Format-Table -AutoSize . Once it is done with those steps, it uses Remove-Job to remove/delete the background job.

Now, for the recursion. The final step inside the if portion of this nested if-else is to invoke the Show-PSGalleryProjectJob function. That is right. The function invokes or calls itself. It starts this whole process over again. It does this using the call operator (&) and the $FunctionName variable. Remember, that variable holds the name of the function. Without the call operator, it would just echo the value in the variable. That operator also called the invocation operator, invokes the function again. Every time the job is completed and the values are returned, the process starts over. On a side note, I have written about recursive functions once before.

Line 14 – 16: The else portion issues a Write-Warning message indicating that the job is not yet complete.
Line 19: The alias invokes the Show-PSGalleryProjectJob function.

The below results show the functions working together. Consider background jobs for longer running tasks and consider recursive functions when a situation presents itself where a function should call, or invoke, itself.

[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 1).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 1).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] # Here is where I waited from some time to pass...
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal

Name                Type   Version Downloads TotalDownloads EndTime
----                ----   ------- --------- -------------- -------
TMOutput            Module 1.1     2994                2994 2/10/2022 5:07:50 PM
Start-1to100Game3.0 Script 3.0     266                 3260 2/10/2022 5:07:50 PM 
Get-TMVerbSynonym   Script 1.4     293                 3553 2/10/2022 5:07:50 PM 
SinkProfile         Module 1.0     304                 3857 2/10/2022 5:07:50 PM 
Show-PSDriveMenu    Script 1.1     186                 4043 2/10/2022 5:07:50 PM 
Switch-Prompt       Script 1.2.0   236                 4279 2/10/2022 5:07:50 PM 

[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 3).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 3).
[PS7.2.1][C:\]

A PowerShell Recursive Function

Note: There is a newer post on tommymaynard.com about a recursive function I wrote. When you are done reading this post, read that one.

I was recently in a position where I was asked to provide an example of a PowerShell recursive function. Unfortunately, I didn’t have time to do that as it was a part of a written and timed test. Sadly, I didn’t even really have time to review the other 50-some answers I had already provided. I mostly knew that might happen.

Anyway, the challenge has been eating at me, so why not write an example and share it. It’ll be good for you, as it will be for me too. It’s out there, but it’s not often utilized that I’m aware. A recursive function is a function that calls, or invokes, itself. For real. We’re not referring to a function, or cmdlet, that includes a Recurse parameter. Now that said, it’s possible that some of these commands, with that parameter, do this (call themselves), but I’ve yet to take a look to see for sure.

After you see today’s example, you’ll begin to understand why you might prefer to do things this way. If you can wrap your head around the concept, you’ll understand that it can reduce a portion of code writing on your part.

As stated, “A recursive function is a function that calls, or invokes, itself.” On that note, let’s first set up the folder structure necessary for this example. While you can do this with PowerShell, I simply didn’t bother. As you can see below, there’s a “Test” folder inside my “Documents” folder. That folder contains three files — A, B, and C.ps1. Additionally, it contains a nested, “More” folder, which contains three more files — D, E, and F.ps1.

So there’s that. Now, let’s take a look at and discuss our PowerShell recursive function.

Function Get-Ps1File {
    Param ($Path= '.\')

    Get-ChildItem -Path $Path | ForEach-Object {
        If ($_.Name -like "*.ps1") {
            $_.Name
        } ElseIf ($_.PSIsContainer) {
            Get-Ps1File -Path $_.FullName
        } # End If-ElseIf.
    } # End ForEach-Object.
} # End Function: Get-Ps1File.

This function loops its way through a path and returns the file(s) that are .ps1 files. If there’s a nested folder, it then goes into that folder and runs itself again (against the content of that folder). Knowing what we know about this folder structure and its files, the below results returned from running this function should make perfect sense.

Get-Ps1File
D.ps1
E.ps1
F.ps1
A.ps1
B.ps1
C.ps1

Let’s recap on this a bit with a little more detail. When the function is first invoked, it uses the current path, as none was supplied. It then runs the ForEach-Object cmdlet against the files in the base folder. Using the If portion of the If-ElseIf statement inside the loop, it determines it should return the A, B, and C .ps1 files. Then, on the fourth iteration through the loop, it hits our “More” folder and determines it’s a folder. There are other ways to determine if it’s a folder, but I stuck with the tried and tested, PSIsContainer NoteProperty.

At this point, it invokes itself against the path of the “More” folder. It runs or executes itself, again. This time, however, it uses the folder’s FullName property (its full path). Inside that second invocation of the function, it iterates over the files inside the “More” folder, echos them to the screen as output, and then ends the function’s second invocation. At that point, it returns back to the first invocation, where it then ends that invocation, as well, as its work is completed.

If you’d prefer to see this sorted, pipe the function name (Get-Ps1File) to the Sort-Object cmdlet.

Get-Ps1File | Sort-Object
A.ps1
B.ps1
C.ps1
D.ps1
E.ps1
F.ps1

And there you go, an example of a PowerShell recursive function. You may not need it often, but you may need it someday.