More Custom Attributes for PowerShell (Parameter Transformation)

I wrote a post awhile back about using custom attributes for PowerShell parameter validation but when I did it, I focused on the use of attributes to improve the error messages output by validation (specifically, by: ValidatePattern).

There are many other things that can be done with custom attributes. However, PowerShell ships with two base types for attributes which derive from the CmdletMetadataAttribute, and it applies special processing to parameters of functions or cmdlets which have these attributes: ValidateArguments and ArgumentTransformation, since I’ve already written about custom argument validation, I figured a post about argument transformations would be appropriate.

It just so happens that I had cause to write such an attribute recently:


using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows.Automation;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class StaticFieldAttribute : ArgumentTransformationAttribute {
   private Type _class;

   public override string ToString() {
      return string.Format("[StaticField(OfClass='{0}')]", OfClass.FullName);
   }

   public override Object Transform( EngineIntrinsics engineIntrinsics, Object inputData) {
      if(inputData is string && !string.IsNullOrEmpty(inputData as string)) {
         System.Reflection.FieldInfo field = _class.GetField(inputData as string, BindingFlags.Static | BindingFlags.Public);
         if(field != null) {
            return field.GetValue(null);
         }
      }
      return inputData;
   }
   
   public StaticFieldAttribute( Type ofClass ) {
      OfClass = ofClass;
   }

   public Type OfClass {
      get { return _class; }
      set { _class = value; }
   }  
}

This was written in C#, but you can wrap it up in an Add-Type -TypeDefinition in PowerShell as I did in the latest preview of my Windows Automation Scripts for PowerShell … and basically embed it in a script, a module, or your profile. A note of warning: you should specify a namespace, really, to avoid collisions.

However, if you can’t read C#, or if you’re not experienced with reflection, you may not even be able to tell what that code does, so let me explain quickly.

Basically, an ArgumentTransformationAttribute is very simple: it just has to have a Transform method which converts an input object into an output object. The Transform method has access to the PowerShell EngineIntrinsics class which gives you access to the current Host, the current SessionState to get variable values, etc. as well as being able to Invoke Commands or PSProviders…

In the example above, I’m trying to transform a string into an object. There’s a collection of the type of objects that I want which are defined as static fields on a certain class, so I created an argument transformation which takes the string, looks for a field with that name, and returns it. I made the class that it looks on configurable, so it would be flexible, so the actual usage looks something like this:

param(
   [Parameter(Mandatory=$false)]
   [System.Windows.Automation.ControlType]
   [StaticField(([System.Windows.Automation.ControlType]))]$ControlType
)

This parameter will now take a ControlType object, or the NAME of one of the fields on ControlType (which it will look up and return), so instead of always having to call: Select-UIElement -ControlType [System.Windows.Automation.ControlType]::Button, I can just write: Select-UIElement -ControlType Button … which is clearly a bit nicer to use.

PowerShell has one of these argument transformations included for use with credentials, so whenever you write a script that has a PSCredential parameter, you should decorate it with the CredentialAttribute like this:

param(
   [Parameter(Mandatory=$false)]
   [System.Management.Automation.PSCredential]
   [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
)

That one’s a little confusing because you leave off the “Attribute” part of the attribute’s name (ie: you don’t have to specify [System.Management.Automation.CredentialAttribute()]), so at first glance, it looks like you’re specifying the Credential type twice. Of course, in reality this is another use of parenthesis in PowerShell. To specify an attribute, you use square braces as with types, but with parenthesis in them (even if the attribute doesn’t require any parameters).

Type then Attribute, and non-mandatory parameters

When you specify a transformation on a parameter, you must be careful to specify the type first and then the transformation attribute (although this may seem counter-intuitive if you’re a developer). This puts the transformation closest to the variable name, and ensures that it is called before the value is cast to the parameter type.

When you don’t specify a parameter as mandatory, and you do specify a transformation attribute, the attribute’s Transform method will still be called, even if the user doesn’t provide you with an input. This is so that the transform attribute can provide a default value if need be. However, it’s called with a null value for the input data — this means that your attribute needs to be able to deal with a null value and output something which the cmdlet or script can deal with.

Depending on your use cases, it may be enough to just output null, but in the case of the Credential parameter, passing an empty string causes the Credential entry dialog to pop up. That’s the desired behavior if you want it to be mandatory, but otherwise, you need to be sure to provide a default value such as the Empty credentials — this will supress the prompt, but it will return an empty credential object you can easily distinguish from a passed-in value. In any case, if nothing is passed and the parameter isn’t marked manadatory, but the transform object creates a default value, the $PSBoundParameters should still be empty.

One Last Hurrah

To make this post as useful as I can, I’ve written a TransformAttribute that takes a script block to do the transform with, and a few examples of using it. In these examples I always assume the input is a string which will be converted into an environment variable, a static field value (this example does exactly the same thing as the class above, but using the generic ScriptBlock-based Transform), and even transforming user names into email addresses (by looking them up in Active Directory). I’ll post the code in PowerShell format this time, with the requisite Add-Type wrapped around it:


Add-Type -TypeDefinition @"
using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Collections.ObjectModel;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class TransformAttribute : ArgumentTransformationAttribute {
   private ScriptBlock _scriptblock;
   private string _noOutputMessage = "
Transform Script had no output.";

   public override string ToString() {
      return string.Format("
[Transform(Script='{{{0}}}')]", Script);
   }

   public override Object Transform( EngineIntrinsics engine, Object inputData) {
      try {
         Collection<PSObject> output =
            engine.InvokeCommand.InvokeScript( engine.SessionState, Script, inputData );
         
         if(output.Count > 1) {
            Object[] transformed = new Object[output.Count];
            for(int i =0; i < output.Count;i++) {
               transformed[i] = output[i].BaseObject;
            }
            return transformed;
         } else if(output.Count == 1) {
            return output[0].BaseObject;
         } else {
            throw new ArgumentTransformationMetadataException(NoOutputMessage);
         }
      } catch (ArgumentTransformationMetadataException) {
         throw;
      } catch (Exception e) {
         throw new ArgumentTransformationMetadataException(string.Format("
Transform Script threw an exception ('{0}'). See `$Error[0].Exception.InnerException.InnerException for more details.",e.Message), e);
      }
   }
   
   public TransformAttribute() {
      this.Script = ScriptBlock.Create("
{`$args}");
   }
   
   public TransformAttribute( ScriptBlock Script ) {
      this.Script = Script;
   }

   public ScriptBlock Script {
      get { return _scriptblock; }
      set { _scriptblock = value; }
   }
   
   public string NoOutputMessage {
      get { return _noOutputMessage; }
      set { _noOutputMessage = value; }
   }  
}
"
@

## Some example transformations:

## Convert a string into the value of the named environment variable (or error)
function Test-TransformEnvironment {
param(
   [Parameter(Mandatory=$true)]
   [string]
   [Transform({ Get-Content "Env:$($args[0])" })]
   $Environment
)
process { Write-Host $Environment }
}

# Test TransformEnvironment
Test-TransformEnvironment UserName
# Test Error Message 1:
Test-TransformEnvironment "This is not an environment variable name"


Add-Type -Assembly UIAutomationTypes
function Test-TransformStaticFieldValue {
param(
   [Parameter(Mandatory=$true)]
   [System.Windows.Automation.ControlType]
   [Transform({
      param([Parameter(Mandatory=$true)][string]$FieldName)
      foreach($field in [System.Windows.Automation.ControlType].GetField( $FieldName, "IgnoreCase,Public,Static" ) | Where { $_ }) {
         $field.GetValue($null)
      }
   })]
   $ControlType
)
process { $ControlType }
}

# Test TransformStaticFieldValue
Test-TransformStaticFieldValue Button
# Test Error Message 2:
Test-TransformStaticFieldValue DoorHandle


function Test-TransformEmail {
param(
   [Parameter(Mandatory=$false)]
   [String[]]
   [Transform(NoOutputMessage = "Specified value is not an email address, and we could not find a user by that name", Script = {
      param([Parameter(Mandatory=$true)][string[]]$UserName)
      if(!$UserName) {
         $UserName = Read-Host "Username"
      }
      $ads = New-Object System.DirectoryServices.DirectorySearcher([ADSI]'')
      foreach($a in $UserName){
         if("$a".Contains("@")) { write-output $a } else {
            $ads.filter = "(|(samAccountName=$a)(displayName=$a))"
            foreach($user in $ads.FindAll().GetEnumerator()) {
               $user.GetDirectoryEntry().Mail
            }
         }
      }
   })]
   $UserEmail
     
)
process { Write-Host $UserEmail }
}

# Test TransformEmail
Test-TransformEmail -User $Env:UserName
# Test Error Message 3:
Test-TransformEmail -User Gremlins

function Test-StackingTransforms {
param(
   [Parameter(Mandatory=$false)]
   [String[]]
   ## Transform UserNames to Email Adresses
   [Transform(NoOutputMessage = "Specified value is not an email address, and we could not find a user by that name", Script = {
      param([Parameter(Mandatory=$true)][string[]]$UserName)
      $ads = New-Object System.DirectoryServices.DirectorySearcher([ADSI]'')
      foreach($a in $UserName){
         if("$a".Contains("@")) { write-output $a } else {
            $ads.filter = "(|(samAccountName=$a)(displayName=$a))"
            foreach($user in $ads.FindAll().GetEnumerator()) {
               $user.GetDirectoryEntry().Mail
            }
         }
      }
   })]
   ## Transform a path to it's content
   [Transform({ if($args[0]) { Get-Content $args[0] } else { Get-Content Env:\Username } })]
   $UserEmail
     
)
process { $UserEmail }
}

## Rely on the default value
Test-StackingTransforms

## Specify an environment variable
Test-StackingTransforms Env:\UserName

## Read from a file (first, build the list)
$Env:UserName > $pwd\users.txt
$Env:UserName >> $pwd\users.txt
$Env:UserName >> $pwd\users.txt
$Env:UserName >> $pwd\users.txt
Test-StackingTransforms $pwd\users.txt
 

I want to point out two things about these examples:

In the TransformAttribute there is custom error handling for the script: exceptions are handled and printed, the case where you get no output is handled and printed, and you can provide your own error message for that no output case.

Notice that not all of the transformed parameters are mandatory: in the last two I’ve dealt with null input specially by either prompting the user from inside the transform script, or using a default value when none is provided. These are powerful options which you should use with care. Prompting from inside the script will allow you to build up pretty much anything you need in the transformation attribute, and having a default value can be just as useful, but you need to make sure that you keep the assumptions in your script in line with what actually happens in the parameter processing.

Hopefully this will give you some ideas for how other transformations :) If you’re inspired, please feel free to share examples in the comments: just use a code tag around them: <code lang="posh">

Similar Posts:

2 thoughts on “More Custom Attributes for PowerShell (Parameter Transformation)”

    1. Not at all :)

      Once this new module is cleaner and working properly, I plan to put that on CodePlex as a new version of the WASP project (I’ll cheat and change the name from “snapin” to “scripts” but shhh!).

      it is my intent to deprecate most of the binary module that I’ve already written and replacing it with the use of System.Windows.UIAutomation rather than Win32 PInvoke calls — the PInvoke stuff doesn’t work well with WPF and even Java apps. The SWA stuff is rather different (with a different paradigm for automation), but it’s actually easier to work with once you get used to it (I think). I can’t promise the new version will be backwards compatible with scripts, though I will do what I can to wrap things up so they work.

Comments are closed.