Multi-Purpose PowerShell Using Function

It’s always bothered me that there isn’t a clean way to deal with IDisposables in PowerShell. It seems Adam Weigert came to the same conclusion and implemented a using function much like the statement found in C# and VB. Note that he also makes use of his implementation of PowerShell try..catch..finally, which is pretty slick. Meanwhile, I'm told Raymond Mitchell has his own using function that he uses to load assemblies, which certainly makes sense to me.

I figure the next evolution is to provide a generic using that covers all the bases:

function using {
    param (
        $inputObject = $(throw "The parameter -inputObject is required."),
        [ScriptBlock] $scriptBlock
    )

    if ($inputObject -is [string]) {
        if (Test-Path $inputObject) {
            [system.reflection.assembly]::LoadFrom($inputObject)
        } elseif($null -ne (
              new-object System.Reflection.AssemblyName($inputObject)
              ).GetPublicKeyToken()) {
            [system.reflection.assembly]::Load($inputObject)
        } else {
            [system.reflection.assembly]::LoadWithPartialName($inputObject)
        }
    } elseif ($inputObject -is [System.IDisposable] -and $scriptBlock -ne $null) {
        Try {
            &$scriptBlock
        } -Finally {
            if ($inputObject -ne $null) {
                $inputObject.Dispose()
            }
            Get-Variable -scope script |
                Where-Object {
                    [object]::ReferenceEquals($_.Value.PSBase, $inputObject.PSBase)
                } |
                Foreach-Object {
                    Remove-Variable $_.Name -scope script
                }
        }
    } else {
        $inputObject
    }
}

Some notes on the code:

  • If $inputObject is a string, I assume it's an assembly reference...
    • If the string is a path, load as a path
    • Rather than parse the string, I figure the framework knows best; the presence of a PublicKeyToken means it's probably a full assembly name
    • I considered adding support for this alternative to LoadWithPartialName, but I don't feel like managing a global "assembly map"; the deprecated shortcut will have to do for now
  • If $inputObject is IDisposable and a script block was supplied...
    • Wrap script execution in Try..Finally to make sure we get to Dispose()
    • Here I disagree with Adam - if the PSObject's Dispose method was overridden, we should assume it was done for good reason (more on this in a later post) and that the override will respect the object's disposability.
    • After disposal, I thought it might be nice to take the variable out of scope like C#/VB. Using -scope script will look at variables in the scope where our function was called, and since we don't know what $inputObject was named before it was passed in, I just compare references instead.
  • Otherwise just punt the object along in the pipeline

Usage

Loading assemblies is pretty straightforward:

using System.Windows.Forms
using 'System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
using C:\Dev\DisposeTest\bin\Debug\DisposeTest.dll

To test IDisposable handling, I use a simple disposable object which returns its hash code in ToString(), instantiated by a helper function:

function new-disp { new-object DisposeTest.Disposable }

To verify that variable scope is handled properly, we need two test scripts. gv is an alias for Get-Variable.

NestedTest.ps1

gv x
using ($x = new-disp) { gv x }
gv x

UsingTest.ps1

$x = 'X'
.\NestedTest.ps1
using ($y = new-disp) { gv y }
gv y

From the behavior in C#/VB, we expect that the object being 'used' will only be available within the scope of the script block. So when we enter NestedTest.ps1, we should see the $x remains 'X', inherited from the parent scope, both before and after the using statement. Similarly, we expect $y will not be accessible outside of the using block:

SharePoint Example

using Microsoft.SharePoint
using ($s = new-object Microsoft.SharePoint.SPSite('http://localhost/')) {
  $s.AllWebs | %{ using ($_) { $_ | select Title, Url } }
}
if($s -eq $null) { 'Success!' }

It's not exceedingly friendly for interactive mode, particularly for tab completion, but it should aid script readability.

Follow

Get every new post delivered to your Inbox.