Eric's Technical Outlet

Learning the hard way so you don't have to

PowerShell: Script that Calls Itself Recursively

To have a PowerShell script that calls itself recursively, use the following construct:

Invoke-Expression -Command $PSCommandPath

If the script uses parameters, use the following construct:

Invoke-Expression -Command ($PSCommandPath + ' -Parameter1 FixedValue -Parameter2 $VariableValue')

If you want to call the script exactly as it was initially called (be careful!):

Invoke-Expression -Command $PSCmdlet.MyInvocation.Line

The benefit to using $PSCommandPath is that you can run the script from anywhere and be certain that it will be able to find itself. This will even survive a rename of the script file.

One thing it doesn’t do is allow for alteration of the parameter names or positions. With a bit of creativity, some of that can be overcome as well. Look in the following variable of a script, and you’ll find the parameter names and their values that were used to start that script:

$MyInvocation.BoundParameters

That only shows the parameters that were used on this particular call. You might want to know what all of them are. You can’t get that from within the script, but there are a couple of ways to find out.

First, if the script has accurate and detailed help, you can do this:

Get-Help -Name $PSCommandPath -Parameter * | select -Property name

If it doesn’t, try this instead:

(Get-Command -Name $PSCommandPath).Parameters[0]

I didn’t try doing this with splatting.

The following script is a demonstration of the concept.

<#
.SYNOPSIS
	Demonstration of a script that recursively calls itself.

.DESCRIPTION
	This script uses recursion to convert a supplied string into its constituent ASCII character codes.

.PARAMETER Word
	The character string to be converted to ASCII.

.PARAMETER AsciiCode
	INTERNAL USE. An ASCII code to be displayed.

.NOTES
	This script is intended for conceptual demonstration purposes only. There are far superior methods to retrieve ASCII codes from a string. See line 33 of this script for one of them.
#>
[CmdletBinding()]
param(
	[Parameter(ParameterSetName="ByWord")]
	[String]$Word = "",

	[Parameter(ParameterSetName="ByCharCode")]
	[Int32]$AsciiCode = 0
)

BEGIN {}

PROCESS {
	function Get-CharCodes
	{
		if($Word.Length -gt 0)
		{
			$CharArray = $Word.ToCharArray()
			foreach($Char in $CharArray)
			{
				$Code = [Int32]$Char
				Invoke-Expression -Command ($PSCommandPath + ' -AsciiCode $Code')
			}
		}
		else
		{
			$AsciiCode
		}
	}
	Get-CharCodes
}

END {}

Notes

Of course, it’s always possible to use recurse a function that’s inside a script just as you would in any programming language. The purpose of calling the entire script is so that you can get a clean processing stack.

I discovered this method while trying to solve a particular problem. I’m currently in the process of designing a script that processes Hyper-V virtual machines. It accepts virtual machine inputs in three ways: the [String] name of the virtual machine, the [Guid] identifier of the virtual machine, or the [Microsoft.HyperV.PowerShell.VirtualMachine] object of the virtual machine. If a user pipes in an array of any of these items, PowerShell is smart enough to split the array first and allow the script to process each one individually — except in one circumstance. If someone uses the [String] parameter to submit wildcards, such as “vm-sql*”, the script will attempt to operate on multiple virtual machines at once.

I had four choices. The first would be to only handle the first virtual machine that fit the wildcard. I discarded that as being just plain unfriendly. The second was to error out the entire script, which was even less friendly than the first. The third was to build in an infrastructure to handle the virtual machines as an array. There’s nothing overtly wrong with that, but, with the functionality I have in mind for this script, it’s likely to be an edge case. I didn’t really want a lot one-item array processing just to be prepared for the < 5% of the time that the input would have more than one item and I dislike all the nesting that goes with it. So, I chose to find a way to recurse the entire script. When a user passes in a [String] with a wildcard, the individual virtual machine objects are passed back into the script as VMObjects.

I had a potential fifth option, and that was to use a separate function inside the same script and call it as necessary. That got messy faster than I would have expected, and I found that $PSCmdlet.ShouldProcess() seemed to always be ignored in that secondary function, which meant that the user would never be prompted before doing some fairly destructive things and that WhatIf wouldn’t work correctly. I haven’t done much research to see if that’s expected behavior. The script-level recursion trick did everything I needed.

Both my real-world use case and my demonstration use case are limited to only recursing once. I can see a bit of a delay when it jumps into that second level, so I would be hesitant to use this form of recursion in a telescoping manner. Remember that setting up a clean environment can be as much of a curse as it is a blessing.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: