add-types.ps1 - poor man's "using" for PowerShell

One thing that's missing from PowerShell is the ability to import foreign namespaces into the current context.  That leads to a lot of typing at the interactive prompt and bloated hard-to-read lines in your scripts.  For example, even if you've loaded the TFS client assemblies, you still have to write out fully-qualified type names like this:

RICBERG470> $itemSpec = new-object Microsoft.TeamFoundation.VersionControl.Client.ItemSpec ("$/foo", [Microsoft.TeamFoundation.VersionControl.Client.RecursionType]::None)

Ick! 

What can we do about it?  Well, the first parameter to new-object is actually a string, so we could store the string "Microsoft.TeamFoundation.VersionControl.Client" somewhere convenient and build from there.

RICBERG470> $vcc = "Microsoft.TeamFoundation.VersionControl.Client"
RICBERG470> $itemSpec = new-object "$vcc.itemspec" ("$/foo", [Microsoft.TeamFoundation.VersionControl.Client.RecursionType]::None)

Better, but far from perfect.

  • It has to be a real variable -- properties and other expressions won't work. 

    RICBERG470> $tfs = get-tfs jpresto-test
    RICBERG470> $tfs | add-member noteproperty vcc "Microsoft.TeamFoundation.VersionControl.Client"
    RICBERG470> new-object "$tfs.vcc.itemspec" ("$/foo", [Microsoft.TeamFoundation.VersionControl.Client.RecursionType]::None)
    New-Object : Cannot find type [jpresto-test.vcc.itemspec]...

  • It won't work when using [] to reference a type.  That will bite us when we want to access a static member/method, or when using enums as in our example.

  • I thought PowerShell was supposed to get us away from string processing, right?

Right!  Namespaces aren't objects in PowerShell ,  so we can't manipulate them directly, but types are.  (They're objects of type RuntimeType, to be exact.)  Objects are PowerShell's bread & butter -- importing a ton of them sounds clunkier than "using <namespace>" in C#, but it's actually a cinch.  I give you add-types.ps1:

 param(
    [string] $assemblyName = $(throw 'assemblyName is required'),
    [object] $object
)

process {
  if ($_) {
     $object = $_
  }
   
    if (! $object) {
        throw 'must pass an -object parameter or pipe one in'
   }
   
    # load the required dll
  $assembly = [System.Reflection.Assembly]::LoadWithPartialName($assemblyName)
    
    # add each type as a member property
 $assembly.GetTypes() | 
 where {$_.ispublic -and !$_.IsSubclassOf( [Exception] ) -and $_.name -notmatch "event"} | 
    foreach { 
        # avoid error messages in case it already exists
     if (! ($object | get-member $_.name)) {
         add-member noteproperty $_.name $_ -inputobject $object
       }
   }
}

Usage example:

RICBERG470> $tfs | add-types "Microsoft.TeamFoundation.VersionControl.Client"
RICBERG470> $itemSpec = new-object $tfs.itemspec("$/foo", $tfs.RecursionType::none)

Much better.  Not only does it work in the [] case, we don't even need the brackets anymore!  Best of all, it works with tab completion.   "RecursionType" above only took 3 keystrokes (and no Shift key).  That's why I filter out exceptions & events, BTW -- they pollute the autocomplete list with zillions of types I'd never use in a script, much less interactively.  If you disagree, feel free to yank those conditions.

TFS readers: if you're with me so far, you'll probably want to edit your copy of get-tfs.ps1.  Just add this line to the end of the foreach block:

 $tfs | add-types $entry[1]
  
 edit 5/15/07: used IsSubClassOf() instead of -notmatch to filter out exceptions.  Thanks to Jay Bazuzi for the suggestion.