Writing stealth code in PowerShell

What happens in module, stays in module.

Most of my scripts are using Import-Component function to bulk-import dependencies (PS1 files with functions, modules, source code, .Net assemblies).

To import PS1 files with functions, they have to be dot-sourced and that provided me with some challenge: if PS1 is dot-sourced inside the function, it will be available only in that function’s scope. To overcome this, I could scope each contained function, alias, and variable as global (nasty!) or call Import-Component function itself using dot-sourcing (yes, you can dot-source more than just files).

For a while, dot-sourcing Import-Component seemed to work fine, until one day, I realized, that this effectively pollutes caller’s scope with all Import-Component‘s internal variables. Consider this example:

function DotSource-Me
{
    $MyString = 'Internal variable'
}

$MyString = 'External variable'

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

If we run this script, the output will be:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains: Internal variable

As you can see, when the DotSource-Me function is called as usual, it’s internal variable is restricted to the function’s scope and doesn’t affect the caller’s scope. But when it’s dot-sourced, variable in the caller’s scope is overwritten.

To remedy this, we could take advantage of the fact, that creating a new module creates an entirely new SessionState. It means that everything that happens inside the module is completely isolated. So if we place all code inside the function in the dynamically generated module, it wouldn’t affect anything outside, even if dot-sourced. Also we don’t want to actually pollute caller’s scope with newly created module object. Luckily for us, the New-Module cmdlet has ReturnResult parameter, that runs the script block and returns the results instead of returning a module object. So lets modify our example:

function DotSource-Me
{
    New-Module -ReturnResult -ScriptBlock {
        $MyString = 'Internal variable'
    }
}

$MyString = 'External variable'

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

And then run it and observe the results:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains: External variable

That’s so much better!

But what if our function that has to be dot-sourced has parameters? Unfortunately, PowerShell will create variable for each parameter, and because function is dot-sourced, those variables will be created in the callers scope:

function DotSource-Me
{
    Param
    (
        $MyString
    )
}

$MyString = 'External variable'

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

And they will pollute and\or overwrite variables in callers scope:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains:

To mitigate this issue, we can exploit the fact that PowerShell doesn’t create corresponding variables for DynamicParameters. Note, that code in the DynamicParam block has to be wrapped in the New-Module too, otherwise it will be executed in caller’s scope

function DotSource-Me
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        New-Module -ReturnResult -ScriptBlock {
            # Set the dynamic parameters name
            $ParameterName = 'MyString'

            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)

            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter(
                        $ParameterName,
                        [string],
                        $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            $RuntimeParameterDictionary
        }
    }
}

$MyString = "External variable"

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

And the result is:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains: External variable

To make things easier, you can put my New-DynamicParameter function inside the New-Module‘ scriptblock and use it like this:

function DotSource-Me
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        New-Module -ReturnResult -ScriptBlock {
            Function New-DynamicParameter
            {
                # function body here...
            }

            New-DynamicParameter -Name MyString -Type ([string])
        }
    }
}

$MyString = "External variable"

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

Bonus chapter

What if we really need to execute something in caller’s scope from the New-Module‘s scriptblock? In Import-Component function, dot-sourcing command itself has to be executed in the caller’s scope, while all other code should be well-hidden in New-Module. To achieve a desired result I’m using a not-so-well-know fact, that scriptblocks are bound to the session state:

Any script block that’s defined in a script or script module (in literal form, not dynamically created with something like [scriptblock]::Create()) is bound to the session state of that module (or to the “main” session state, if not executing inside a script module.) There is also information specific to the file that the script block came from, so things like breakpoints will work when the script block is invoked.

When you pass in such a script block as a parameter across script module boundaries, it is still bound to its original scope, even if you invoke it from inside the module.

Here is the final example:

function DotSource-Me
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        New-Module -ReturnResult -ScriptBlock {
            # Set the dynamic parameters name
            $ParameterName = 'ScriptBlock'

            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)

            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter(
                        $ParameterName,
                        [scriptblock],
                        $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            $RuntimeParameterDictionary
        }
    }

    Process
    {
        New-Module -ReturnResult -ScriptBlock {
            # Assign internal variable
            $MyString = "Internal variable"

            # Execute scriptblock
            & $PSBoundParameters.ScriptBlock
        }
    }
}

$MyString = "External variable"
$MyScriptBlock = {Write-Host "Scriptblock, 'MyString' contains: $MyString"}

Write-Host "Script, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me -ScriptBlock $MyScriptblock

Note that although $MyString variable is defined inside the New-Module‘s scriptblock, the code in the MyScriptBlock‘s parameter’s scriptblock is executed in the caller’s scope and accesses $MyString variable from there:

Script. 'MyString' contains: External variable
Scriptblock. 'MyString' contains: External variable

Dynamic parameters, ValidateSet and Enums

Good intentions often get muddled with very complex execution. The last time the government tried to make taxes easier, it created a 1040 EZ form with a 52-page help booklet.
— Brad D. Smith

I suppose that many of you have heard about Dynamic Parameters, but thought of them as too complicated to implement in real-life scenarios. Just look at the amount of code you have to write to add one simple parameter with dynamic ValidateSet argument.

Recently I had to write a fair amount of functions which use Enum‘s values as parameters (Special Folders, Access Rights, etc). Naturally, I’d like to have this parameters validated with ValidateSet and have tab-completion as a bonus. But this means to hardcode every enum’s member name in the ValidateSet argument. Today’s example is a function, that returns Special Folder path. It accepts one parameter Name, validates it values against all known folders names and returns filesystem paths. Here is how it looks with hardcoded ValidateSet:

function Get-SpecialFolderPath
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet(
            'Desktop', 'Programs', 'MyDocuments', 'Personal', 'Favorites', 'Startup', 'Recent', 'SendTo',
            'StartMenu', 'MyMusic', 'MyVideos', 'DesktopDirectory', 'MyComputer', 'NetworkShortcuts', 'Fonts',
            'Templates', 'CommonStartMenu', 'CommonPrograms', 'CommonStartup', 'CommonDesktopDirectory',
            'ApplicationData', 'PrinterShortcuts', 'LocalApplicationData', 'InternetCache', 'Cookies', 'History',
            'CommonApplicationData', 'Windows', 'System', 'ProgramFiles', 'MyPictures', 'UserProfile', 'SystemX86',
            'ProgramFilesX86', 'CommonProgramFiles', 'CommonProgramFilesX86', 'CommonTemplates', 'CommonDocuments',
            'CommonAdminTools', 'AdminTools', 'CommonMusic', 'CommonPictures', 'CommonVideos', 'Resources',
            'LocalizedResources', 'CommonOemLinks', 'CDBurning'
        )]
        [array]$Name
    )

    Process
    {
        $Name | ForEach-Object { [Environment]::GetFolderPath($_) }
    }
}

Not fancy, to say the least.


Sidenote: if you wonder, did I typed all this ValidateSet argument, the answer is no. Here is trick that I’ve used to get all enum’s members strings enclosed in single quotes and comma-separated. Just copy and paste this snippet to the PowerShell console and get formatted enum list in your clipboard:

PS C:\Users\beatcracker> "'$([Enum]::GetNames('System.Environment+SpecialFolder') -join "', '")'" | clip

As you see, the ValidateSet above is as bad as you can get: it’s large, it’s easy to make typo and it’s hardcoded. Whenever the new special folder is added to the Windows or it doesn’t exists in previous versions of OS this code will fail.

Let’s try to remedy this by using Dynamic Parameters. The following example is based on aforementioned TechNet article Dynamic ValidateSet in a Dynamic Parameter.

function Get-SpecialFolderPath
{
    [CmdletBinding()]
    Param()
    DynamicParam 
    {
        # Set the dynamic parameters name
        $ParameterName = 'Name'

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create the collection of attributes
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.ValueFromPipeline = $true
        $ParameterAttribute.ValueFromPipelineByPropertyName = $true
        $ParameterAttribute.Mandatory = $true
        $ParameterAttribute.Position = 0

        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)

        # Generate and set the ValidateSet
        $arrSet = [Enum]::GetNames('System.Environment+SpecialFolder')
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)

        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)

        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
        return $RuntimeParameterDictionary
    }

    Begin
    {
        # Bind the parameter to a friendly variable
        $Name = $PsBoundParameters[$ParameterName]
    }

    Process
    {
        $Name | ForEach-Object { [Environment]::GetFolderPath($_) }
    }
}

This version of function definitely better than the first one: no hardcoded values and it wouldn’t break on different versions of Windows. But it still feels clumsy to me and my inner perfectionist. The whole DynamicParam block is dedicated to the single parameter and modifying it to suit your needs may be a job worth a day.

What if you’d like to define multiple parameters dynamically, with different arguments (Mandatory, ValueFromPipeline, etc), belonging to different parameter sets and so on. Moreover, you’d like to do it in fast and efficient way.

The solution? Dynamically create dynamic parameters! Luckily, I was not a first preson to think about this and there are folks who have done tremendous job already: Justin Rich (blog, GitHub) and Warren F. (blog, GitHub):

So all I had to do is to improve their work a little. I’ve took a liberty to extend Warren’s New-DynamicParam function to support full range of attributes and made a recreation of the variables from the bound parameters a bit easier:

It drastically reduces the amount of hoops you’d have to jump through and makes your code clean and crisp. Let’s see how we can use it to create dynamic parameters from enum values in our Get-SpecialFolderPath function:

function Get-SpecialFolderPath
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        # Get special folder names for ValidateSet attribute
        $SpecialFolders = [Enum]::GetNames('System.Environment+SpecialFolder')

        # Create new dynamic parameter
        New-DynamicParameter -Name Name -ValidateSet $SpecialFolders -Type ([array]) `
        -Position 0 -Mandatory -ValueFromPipeline -ValueFromPipelineByPropertyName -ValidateNotNull
    }

    Process
    {
        # Bind dynamic parameter to a friendly variable
        New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters

        $Name | ForEach-Object { [Environment]::GetFolderPath($_) }
    }
}

As you see, it takes only three lines of code to create new dynamic parameter. But the more the merrier, so how about several dynamic parameters?

Here is the example taken directly from the help of the New-DynamicParameter function. It will create several dynamic parameters, with multiple Parameter Sets.

In this example three dynamic parameters are created. Two of the parameters are belong to the different parameter set, so they are mutually exclusive. One of the parameters belongs to both parameter sets.

  • The Drive‘s parameter ValidateSet is populated with all available volumes on the computer.
  • The DriveType‘s parameter ValidateSet is populated with all available drive types.
  • The Precision‘s parameter controls number of digits after decimal separator for Free Space percentage.

Usage:

PS C:\Users\beatcracker> Get-FreeSpace -Drive <tab> -Precision 2
PS C:\Users\beatcracker> Get-FreeSpace -DriveType <tab> -Precision 2

Parameters are defined in the array of hashtables, which is then piped through the New-Object to create PSObject and pass it to the New-DynamicParameter function.

If parameter with the same name already exist in the RuntimeDefinedParameterDictionary, a new Parameter Set is added to it.

Because of piping, New-DynamicParameter function is able to create all parameters at once, thus eliminating need for you to create and pass external RuntimeDefinedParameterDictionary to it.

function Get-FreeSpace
{
	[CmdletBinding()]
	Param()
	DynamicParam
	{
		# Array of hashtables that hold values for dynamic parameters
		$DynamicParameters = @(
			@{
				Name = 'Drive'
				Type = [array]
				Position = 0
				Mandatory = $true
				ValidateSet = ([System.IO.DriveInfo]::GetDrives()).Name
				ParameterSetName = 'Drive'
			},
			@{
				Name = 'DriveType'
				Type = [array]
				Position = 0
				Mandatory = $true
				ValidateSet = [System.Enum]::GetNames('System.IO.DriveType')
				ParameterSetName = 'DriveType'
			},
			@{
				Name = 'Precision'
				Type = [int]
				# This will add a Drive parameter set to the parameter
				Position = 1
				ParameterSetName = 'Drive'
			},
			@{
				Name = 'Precision'
				# Because the parameter already exists in the RuntimeDefinedParameterDictionary,
				# this will add a DriveType parameter set to the parameter.
				Position = 1
				ParameterSetName = 'DriveType'
			}
		)

		# Convert hashtables to PSObjects and pipe them to the New-DynamicParameter,
		# to create all dynamic parameters in one function call.
		$DynamicParameters | ForEach-Object {New-Object PSObject -Property $_} | New-DynamicParameter
	}
	Process
	{
		# Dynamic parameters don't have corresponding variables created,
		# you need to call New-DynamicParameter with CreateVariables switch to fix that.
		New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters

		if($Drive)
		{
			$Filter = {$Drive -contains $_.Name}
		}
		elseif($DriveType)
		{
			$Filter = {$DriveType -contains  $_.DriveType}
		}

		if(!$Precision)
		{
			$Precision = 2
		}

		$DriveInfo = [System.IO.DriveInfo]::GetDrives() | Where-Object $Filter
		$DriveInfo |
			ForEach-Object {
				if(!$_.TotalFreeSpace)
				{
					$FreePct = 0
				}
				else
				{
					$FreePct = [System.Math]::Round(($_.TotalSize / $_.TotalFreeSpace), $Precision)
				}
				New-Object -TypeName psobject -Property @{
					Drive = $_.Name
					DriveType = $_.DriveType
					'Free(%)' = $FreePct
				}
			}
	}
} 

There is more examples in the help, that should get you started with Dynamic Parameters in no time. If something doesn’t work for you, feel free to drop me a note, I’ll be happy to fix it.