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.

Continue reading

Advertisements

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.

Continue reading