Powershell parameter namespace collision

I'm a Powershell beginner, although not a programming n00b. I'm trying to create an IDisposable/RAII-style failsafe pattern, sort of like in:

http://www.sbrickey.com/Tech/Blog/Post/IDisposable_in_PowerShell

So I have:

Function global:FailSafeGuard
{
param (
[parameter(Mandatory=$true)] [ScriptBlock] $execute,
[parameter(Mandatory=$true)] [ScriptBlock] $cleanup
)

    Try { &$execute }
    Finally { &$cleanup }
}

I'm trying to use it to perform a bunch of tasks in a different directory, using Push-Location on the way in and Pop-Location on the way out. So I have:

Function global:Push-Location-FailSafe
{
param (
$location,
[ScriptBlock] $execute
)
    FailSafeGuard {
        Push-Location $location;
        &$execute
        } { Pop-Location }  
}

I find that the $execute param in Push-Location-FailSafe collides with the $execute param in the FailSafe function.

Push-Location-FailSafe "C:\" {dir}
The expression after '&' in a pipeline element produced an invalid object. It must result in a command name, script block or CommandInfo object.
At C:\TEMP\b807445c-1738-49ff-8109-18db972ab9e4.ps1:line:20 char:10
+         &$ <<<< execute

The reason I think it's a name-collision is that if I rename $execute to $execute2 in Push-Location-FailSafe, it works fine:

Push-Location-FailSafe "C:\" {dir}
    Directory: C:\

Mode                LastWriteTime     Length Name   
----                -------------     ------ ----   
d----        2011-08-18     21:34            cygwin 
d----        2011-08-17     01:46            Dell   
[snip]

What's wrong in my understanding of parameters?

Upvotes: 1

Views: 564

Answers (1)

Frode F.
Frode F.

Reputation: 54911

Your problem is with scriptblocks and how they handle variables. Variables inside a scriptblock doesn't expand until they are executed. Because of this you are hitting a loop. Let me show you:

When you call Push-Location-Failsafe method your variable is like this:

[DBG]: PS C:\>> (Get-Variable execute).Value
 dir 

But then you call your inner function FailSafeGuard, your $execute variable changes to this:

[DBG]: PS C:\>> (Get-Variable execute).Value

        Push-Location $location;
        & $execute

Now when you're try { } block starts executing, it begins to expand the variables. When it expands $execute it will get look like this:

Try { 
    Push-Location $location;
    & $execute 
}

Then it expands $execute again. Your try block is now:

Try { 
    Push-Location $location;
    & {
        Push-Location $location;
        & $execute
      } 
}

And you got yourself an infinite loop caused by recursion. To fix this, you can expand your $execute variable inside a string, that you then create a scriptblock out of. Like this:

Function global:Push-Location-FailSafe
{
param (
$location,
[ScriptBlock] $execute
)
    FailSafeGuard ([ScriptBlock]::Create("
        Push-Location $location;
        & $execute")) { Pop-Location }  
}

Be aware that this particular solution will have a problem when $execute includes variables inside. e.g.: $execute = { $h = dir } because it will try to expand $h when it creates the scriptblock.

An easier and better way to solve it is just to use different variablenames so there's no collision in the first place :-)

Upvotes: 1

Related Questions