Custom type extensions - PowerShell scripting and automation - PowerShell in Depth, Second Edition (2015)

PowerShell in Depth, Second Edition (2015)

Part 3. PowerShell scripting and automation

Chapter 27. Custom type extensions

This chapter covers

· Using PowerShell’s Extensible Type System

· Creating custom type extensions

· Importing custom type extensions

· Creating custom type extensions dynamically

Windows PowerShell includes a feature known as the Extensible Type System (ETS). A type, in PowerShell’s world, is a data structure that describes what a type of data looks like, as well as what capabilities that type of data has. For example, there’s a type that represents a Windows service. The type says that a service has a name, a description, a status, and other properties. The type also says that a service has methods enabling you to stop the service, start it, pause it, resume it, and so on.

Types are generally born within the .NET Framework that lies underneath PowerShell.

Warning

The objects returned by PowerShell aren’t necessarily pure .NET objects. In many cases the PowerShell wrapper will add, or hide, properties and methods. Use the options on the –View parameter of Get-Member to determine the modifications that have been performed.

So, the properties and methods—collectively, the members—of a type are defined in the Framework and carried up through PowerShell to you. You can visit http://msdn.microsoft.com/en-us/library/system.serviceprocess.servicecontroller.aspx to see Microsoft’s .NET Framework documentation for a service, and you’ll see its properties, methods, and so on.

27.1. What are type extensions?

PowerShell has the ability to extend a type, adding more members to it. In most cases, these type extensions are intended to make the type easier to use or more consistent. For example, most types in the .NET Framework include a Name property, and PowerShell relies on that for several tasks. When you use Format-Wide, for example, it defaults to displaying a type’s Name property. But services don’t have a Name property—for some reason, they were given a ServiceName property. That makes a service more difficult to use because it isn’t consistent with the rest of the Framework. So, PowerShell extends the service type, adding a Name property. Technically, it’s an AliasProperty because it simply points to the existing ServiceName property. But it helps make the type more consistent.

PowerShell can add a number of extensions to a type:

· DefaultDisplayPropertySet—For types that don’t have a defined default view, the DefaultDisplayPropertySet tells PowerShell which of a type’s properties to display by default. If this set includes more than four properties, they’ll be displayed as a list; otherwise, PowerShell uses a table. Run Get-WmiObject –Class Win32_OperatingSystem to see a DefaultDisplayPropertySet in action.

Note

Technically, DefaultDisplayPropertySet is a kind of extension called a PropertySet. You can create other PropertySet extensions apart from a DefaultDisplayPropertySet. Practically speaking, there’s little reason to do so. Microsoft originally had plans for other kinds of PropertySet extensions, but those were never implemented.

· AliasProperty—This extension points to another property. It doesn’t rename the original property because the original also remains accessible; it simply provides access to the property under an alternate name. Usually the alias is something that makes more sense to you, like Nameinstead of ServiceName. Or it can be a shortcut, like VM instead of VirtualMemorySize.

· NoteProperty—This extension adds a property that contains static—that is, unchanging—information. You don’t see these used a lot in predefined type extensions because you don’t often want to add an unchanging piece of information to a type. You’ll mainly see NotePropertyused for dynamically generated types, such as the types created by a command like Get-Service | Select-Object –property Name. You’ll also see this used when creating your own custom objects.

· ScriptProperty—This extension adds a property whose value is determined dynamically by an embedded PowerShell script. In other words, when you access the property, a short PowerShell script will run to produce the property’s value. Microsoft uses these a lot, especially to provide easy access to some piece of data that’s otherwise a bit buried. For example, when you run Get-Process, the output objects will include several ScriptPropertys. These provide access to information about a process that would otherwise be difficult to retrieve.

· ScriptMethod—This extension is similar to a ScriptProperty: When you execute a ScriptMethod, an embedded PowerShell script runs to take whatever action the method provides.

· CodeMethod—This extension executes a static method of a .NET Framework class. A static method is one that can be executed without creating an instance of the class. The Math class, for example, has a number of static methods that perform various mathematical operations.

· CodeProperty—This extension accesses a static property of a .NET Framework class.

In this book, we’ll focus primarily on the extensions that get the most use by administrators: We’ll show you how to create a DefaultDisplayPropertySet, an Alias-Property, a ScriptProperty, and a ScriptMethod.

27.2. Creating and loading a type extension file

Type extensions are defined in a special kind of XML file, which usually has a .ps1xml filename extension. You can see an example of a type extension file by running these two commands:

PS C:\> cd $pshome

PS C:\Windows\System32\WindowsPowerShell\v1.0> notepad .\types.ps1xml

Warning

Don’t change, even in the slightest way, any of the files in PowerShell’s installation folder, including types.ps1xml. These files carry a Microsoft digital signature. Altering the file will break the signature, preventing the entire file from being used. Even something as minor as an extra space or carriage return will render the file useless.

A type extension file starts off with the following XML, which you can copy from types.ps1xml:

<?xml version="1.0" encoding="utf-8" ?>

<Types>

</Types>

Note

You can use the PowerShell ISE, Notepad, or almost any text editor to create and edit type extension files.

Between the <Types> and </Types> tags is where you place your individual type extensions. Once you’ve created a file, you’re ready to load it into PowerShell to test it. PowerShell loads only Microsoft-provided type extensions by default; when you import a module or add a PSSnapin, they can also include type extensions that PowerShell loads into memory. To load your own extensions (that aren’t part of a module), you run the Update-TypeData cmdlet. This requires that you use either the –PrependPath or –AppendPath parameter, and both parameters accept the path and filename of your type extension XML file. Which do you choose? It depends on what you want your extensions to do.

Note

Keep in mind that your extensions last only as long as the current shell session. Once you close the shell, your extensions are removed from memory until you reload them. So if you mess up, in the worst case close the shell and open a new shell window to start fresh. If you want the extensions to be always loaded, use Update-TypeData in your profile.

When PowerShell is ready to add extensions to a type, it looks at the type extensions it has loaded in memory. It scans them in the order in which they were loaded into memory, and the first one it finds that matches the type it’s working with is the only one it uses. Imagine that you’re providing an extension for a type that Microsoft already provided an extension for. If you load your XML file with the –PrependPath parameter, then your extension will be first in memory, so PowerShell will use it. In other words, your extensions are prepended to the ones already in memory. On the other hand, if you use–AppendPath, then your extension will be last in memory, and it won’t be used if Microsoft has already provided an extension for that type. So the rules are these:

· If you’re extending a type that already has an extension, prepend your extension into memory.

· If you’re extending a type that doesn’t already have an extension, append your extension into memory.

· If you’re not sure, try prepending. You may find that some functionality of a type was being provided by a Microsoft-supplied extension, and you’ll lose that functionality unless your type extension duplicates it.

Note

It’s perfectly safe to go into Microsoft’s types.ps1xml file, copy things from it, and paste them into your own type extensions. That’s one way of providing additional extensions while retaining the ones Microsoft created. You’d then prepend your extensions into memory.

When you run Update-TypeData, it’ll first parse your file to see if the XML makes sense. This is where errors are most likely to creep in. Read the error carefully; it’ll tell you the exact line of the file that PowerShell has a problem with. Unfortunately, you’ll have to close PowerShell at this point because it won’t let you try to load the same file a second time. For that reason, we tend to put our type extension files into an easy-to-reach folder like C:\ because we often have to open a shell window, load our extension, read the error, and close the shell several times before we finally get it right.

Once the type extension is working to your satisfaction, and you need it to be loaded every time you start PowerShell, you can add a command to your profile to perform the load. It’s better to make the type extension part of a module and load it as required.

Tip

Unlike almost everything else in PowerShell, the XML files are case sensitive. <types> isn’t the same as <Types>, and the former will generate an error. Be careful! If you use the PowerShell ISE, watch for IntelliSense errors regarding missing tags.

27.3. Making type extensions

Within the <Types> and </Types> tags of your XML file, you’ll create your type extensions. We’ll go through each of the major types and provide you with examples to see them in action.

All of the extensions associated with a single type, such as a process, go into a single block within the XML file. That basic block, which will always be your starting point for new extensions, looks like this:

<Type>

<Name>type_name_goes_here</Name>

<Members>

</Members>

</Type>

You can see where the type name goes. This is the exact and full name of the type, as revealed by Get-Member. For example, run Get-Process | Get-Member and the first line of output will be

TypeName: System.Diagnostics.Process

That’s the type name you’d put between the <Name> and </Name> tags. Remember, the tags are case sensitive! From there, all of the extensions for this type would appear between the <Members> and </Members> tags.

Note

For the four following examples, we’ll focus on the Process type. This is a type already extended by Microsoft, and when you prepend your extension, you’re going to essentially turn off Microsoft’s extensions. That’s okay. Closing and reopening the shell will put Microsoft’s extensions back into place.

27.3.1. AliasProperty

Remember that an alias simply defines an alternate name for accessing an existing property. An AliasProperty extension looks like this:

<AliasProperty>

<Name>PID</Name>

<ReferencedMemberName>Id</ReferencedMemberName>

</AliasProperty>

You’ve just created a new property called PID, which will point to the underlying ID property of the process type.

27.3.2. ScriptProperty

A ScriptProperty is a value that’s calculated or returned from executing a small PowerShell expression. A ScriptProperty extension looks like this:

<ScriptProperty>

<Name>Company</Name>

<GetScriptBlock>

$this.Mainmodule.FileVersionInfo.CompanyName

</GetScriptBlock>

</ScriptProperty>

You’re adding a new property called Company (this extension is one of the ones Microsoft provides for the process type). It runs a script, which uses the special $this variable (see about_Automatic_Variables). That variable always refers to the current instance of the type. In other words, when you’ve created a process object—usually by running Get-Process, which creates several objects—$this will refer to a single process. Here you’re using it to access the process type’s native Mainmodule.FileVersionInfo.CompanyName property. In other words, you’re not running any .NET Framework code; you’re simply providing an easier way to access a deeply nested property that’s already there.

27.3.3. ScriptMethod

A ScriptMethod is a method that you define in your extension. The method’s action is coded by one or more PowerShell commands. Here’s what a ScriptMethod looks like:

<ScriptMethod>

<Name>Terminate</Name>

<Script>

$this.Kill()

</Script>

</ScriptMethod>

This isn’t a fancy ScriptMethod: You’re simply creating a method called Terminate() that executes the object’s Kill() method. Kill just seemed so forceful and gritty that we felt more comfortable with the softer, friendlier-sounding Terminate(). Your ScriptMethods can contain much more complicated scripts, if needed, although we’ll again point out that the $this variable provides access to the current object instance.

ScriptMethod or ScriptProperty?

The difference between a ScriptProperty and a ScriptMethod can be somewhat arbitrary. Under the hood, the .NET Framework doesn’t technically have properties—they’re implemented as methods. So the line is blurry all the way down! Which you choose to use depends on how you plan to use whatever the extension produces.

If you plan to display information as part of the type’s normal output, such as in a list or a table, you want to make a ScriptProperty. Like all the other properties available in PowerShell, a ScriptProperty can be used with Format cmdlets, with Select-Object, and with other cmdlets that choose rows and columns to display.

If you’re planning on filtering objects based on the contents of something, a ScriptProperty will do the job. So, if you can imagine your data being used as criteria in a Where-Object cmdlet, use a ScriptProperty to expose that data.

A ScriptMethod is generally used when you need to access outside information, such as getting data from another computer, from a database, and so on. A ScriptMethod is also a good choice when you’re transforming data, such as changing the format of a date or time. You’ll notice that every object produced by Get-WmiObject, for example, includes a couple of ScriptMethods for reformatting dates. The Convert-ToDateTime script method is useful and saves a lot of additional effort.

27.3.4. DefaultDisplayPropertySet

Because Microsoft once had big plans for property sets—most of which were never realized—the XML for creating a DefaultDisplayPropertySet is a bit more complicated than you might think necessary. A property set is a collection of properties that can be referenced by a single property name. The DefaultDisplayPropertySet is therefore made up of a few other properties, hence the complexity:

<MemberSet>

<Name>PSStandardMembers</Name>

<Members>

<PropertySet>

<Name>DefaultDisplayPropertySet</Name>

<ReferencedProperties>

<Name>ID</Name>

<Name>Name</Name>

</ReferencedProperties>

</PropertySet>

</Members>

</MemberSet>

We know, it’s a lot. You’re just worried about the <Name></Name> tag pairs that identify the properties you want displayed by default. Here, you’re identifying two: Name and ID. Because that’s less than five, it’ll be displayed as a table. But keep in mind that PowerShell doesn’t even look for a DefaultDisplayPropertySet unless it can’t find a predefined view. In the case of the process type, there’s a predefined view, which constructs the familiar multicolumn table that you see when you run Get-Process. As a result, your DefaultDisplayPropertySet won’t have any effect on the shell’s operation or output.

27.4. A complete example

Next, you’ll create a short script that illustrates how these type extensions are used. This isn’t how you’d normally deploy a type extension; it’s preferable to load them as part of a module. In chapter 32 that’s exactly what you’ll do. For now, let’s keep things simple and have the script load the type extension file every time you run it.

First, create the type extension file shown in listing 27.1. Save it as OurTypes .ps1xml in your C directory. Next, create the script shown in listing 27.2, which uses the type extension. This type extension is for a new type that you’re creating in your script.

Listing 27.1. OurTypes.ps1xml

<?xml version="1.0" encoding="utf-8" ?>

<Types>

<Type>

<Name>OurTypes.Computer</Name>

<Members>

<AliasProperty>

<Name>Host</Name>

<ReferencedMemberName>ComputerName</ReferencedMemberName>

</AliasProperty>

<ScriptProperty>

<Name>MfgModel</Name>

<GetScriptBlock>

$this.Model + ' ' + $this.Manufacturer

</GetScriptBlock>

</ScriptProperty>

<ScriptMethod>

<Name>IsReachable</Name>

<Script>

Test-Connection $this.computername -quiet

</Script>

</ScriptMethod>

<MemberSet>

<Name>PSStandardMembers</Name>

<Members>

<PropertySet>

<Name>DefaultDisplayPropertySet</Name>

<ReferencedProperties>

<Name>ComputerName</Name>

<Name>MfgModel</Name>

</ReferencedProperties>

</PropertySet>

</Members>

</MemberSet>

</Members>

</Type>

</Types>

Listing 27.2. The type extension test script

param([string]$computername)

Update-TypeData -AppendPath C:\OurTypes.ps1xml -EA SilentlyContinue

$bios = Get-WmiObject -Class Win32_BIOS -ComputerName $computername

$cs = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $computername

$properties = @{ComputerName=$computername

Manufacturer=$cs.manufacturer

Model=$cs.model

BIOSSerial=$bios.serialnumber}

$obj = New-Object -TypeName PSObject -Property $properties

$obj.PSObject.TypeNames.Insert(0,"OurTypes.Computer")

Write-Output $obj

Notice that the script in listing 27.2 uses –EA SilentlyContinue when it attempts to load the type extension. That’s because you’ll get an error if you try to load an extension that’s already in memory. For this simple demonstration, you’re suppressing the error.

Running your script produces the following output:

ComputerName MfgModel

------------ --------

localhost VMware Virtual Platform VMware, Inc.

If you pipe your script’s output to Get-Member, you’ll see this:

TypeName: OurTypes.Computer

Name MemberType Definition

---- ---------- ----------

Host AliasProperty Host = ComputerName

Equals Method bool Equals(System.Object obj)

GetHashCode Method int GetHashCode()

GetType Method type GetType()

ToString Method string ToString()

BIOSSerial NoteProperty System.String BIOSSerial=VMware-56 4d 47 10...

ComputerName NoteProperty System.String ComputerName=localhost

Manufacturer NoteProperty System.String Manufacturer=VMware, Inc.

Model NoteProperty System.String Model=VMware Virtual Platform

IsReachable ScriptMethod System.Object IsReachable();

MfgModel ScriptProperty System.Object MfgModel {get=$this.Model + '...

You can see that your ScriptMethod and ScriptProperty are both there, as well as your AliasProperty. Your default display only included the two properties you specified as your DefaultDisplayPropertySet. You can also see your ScriptMethod in action:

PS C:\> $object = ./test localhost

PS C:\> $object.IsReachable()

True

Now, we do have to admit to something: Given that the object produced by the script was created by that script, we could’ve had you add your AliasProperty, Script-Property, and ScriptMethod right in the script. We suppose you could’ve added theDefaultDisplayPropertySet that way too, although the syntax is pretty complicated. So why bother with the XML file? Because you may produce this same type of object in other scripts. You can also control the default data displayed by your object. By defining these type extensions in the XML file and loading that into memory, you’re applying your extensions to this object type no matter where it’s created. It’s a much more centralized way of doing things, and it keeps your script nice and easy to read.

27.5. Updating type data dynamically

If you’ve made it this far into the chapter, you’re probably thinking that creating custom type extensions is a lot of work. Well, it doesn’t have to be. One of the reasons we went through the previous material is so that you understand how it all works. Now that you do, we’ll show you some easier ways to add custom type extensions that were introduced in PowerShell v3.

Earlier we showed you how to use Update-TypeData to load type extensions from an XML file. But you can also use the cmdlet to define a type extension for the current PowerShell session without an XML file. You’ll need to know the type name, which you can get by piping an object toGet-Member, the type of member (such as Script-Property), and a value. Here’s a quick example:

Update-TypeData -TypeName system.io.fileinfo `

-MemberType ScriptProperty -MemberName IsScript -Value {

$extensions=".ps1",".bat",".vbs",".cmd";

if ($this.extension -in $extensions) {$True} else {$False}

}

This command is adding a ScriptProperty to the file object type. The name of this new member is IsScript. The value will be calculated for each file by testing if the file extension of the current object ($this) is in a defined list of script extensions. If it is, the value will be True.

Once it’s loaded, you can run a command like this:

PS C:\> dir c:\work\ -file | select Name,IsScript

Name IsScript

---- --------

a.xml False

AccessMaskConstants.ps1 True

acl-formatdemo.ps1 True

add-managemember.ps1 True

add-managemember2.ps1 True

Audit.ps1 True

b.txt False

b.xml False

Backup-EventLog.ps1 True

Backup-EventLogv2.ps1 True

Backup-FolderToCloud.ps1 True

Backup-VM.ps1 True

BackupAllEventLogs.ps1 True

...

Remember, the new property isn’t part of the default display so you need to specify it.

For quick, ad hoc type extensions, this approach is handy. You can also redefine types without having to start a new PowerShell session. If your extension doesn’t work the way you want, revise and add it again, but use the –Force parameter to overwrite the existing type extension.

You can’t accomplish everything that you can in an XML file, and if you need to define multiple members, you’ll need multiple commands. The following listing demonstrates how to add several new type extensions.

Listing 27.3. Adding dynamic type extensions

Listing 27.3 first defines new properties (, ). The existing properties are formatted in the less-than-friendly WMI date time format. We like easy-to-read date times, so this listing uses the ConvertToDateTime() method that’s part of every WMI object in PowerShell and converts the existing value. You can’t overwrite the existing value, LastBoot-UpTime, because you’ll end up in a loop. That’s why you created new properties.

You then created alias properties (, , ). Some WMI property names are less than meaningful. Finally, you redefined the default display property to use the new properties. This means that when you display a Win32_OperatingSystem object, you’ll get a new default display. You might’ve preferred to create a PropertySet, which would leave the default intact. But you can’t do that dynamically. This is a situation where using an XML file would be a better solution.

You can confirm the changes by looking at an instance of Win32_OperatingSystem with Get-Member:

PS C:\> get-wmiobject win32_operatingsystem |

>> get-member -MemberType AliasProperty,ScriptProperty

TypeName:

System.Management.ManagementObject#root\cimv2\Win32_OperatingSystem

Name MemberType Definition

---- ---------- ----------

Computername AliasProperty Computername = CSName

OperatingSystem AliasProperty OperatingSystem = Caption

PSComputerName AliasProperty PSComputerName = __SERVER

ServicePack AliasProperty ServicePack = CSDVersion

Installed ScriptProperty System.Object Installed {get=

$This.Conve...

LastBoot ScriptProperty System.Object LastBoot {get=

$This.Conver...

And here’s what it looks like in action:

PS C:\> get-wmiobject win32_operatingsystem

Computername : RSSURFACEPRO2

Operatingsystem : Microsoft Windows 8.1 Pro

ServicePack :

OSArchitecture : 64-bit

Installed : 05/12/2013 10:16:49

LastBoot : 27/01/2014 17:27:00

These extensions will remain for the duration of the current PowerShell session.

27.6. Get-TypeData

Another feature introduced in PowerShell v3 is the ability to examine all of the currently installed type extensions with Get-TypeData:

PS C:\> get-typedata

TypeName Members

-------- -------

System.Array {[Count, System.Management.Au...

System.Xml.XmlNode {[ToString, System.Management...

System.Xml.XmlNodeList {[ToString, System.Management...

System.Management.Automation.PSDriveInfo {[Used, System.Management.Aut...

System.DirectoryServices.PropertyValu... {[ToString, System.Management...

System.Drawing.Printing.PrintDocument {[Name, System.Management.Aut...

System.Management.Automation.Applicat... {[FileVersionInfo, System.Ma...

System.DateTime {[DateTime, System.Managemen...

System.Net.IPAddress {[IPAddressToString, System.M...

...

The output is a TypeData object. Let’s look at the WMI object you modified in the previous section:

PS C:\> $type = 'System.Management.ManagementObject#

root\cimv2\Win32_OperatingSystem'

PS C:\> Get-TypeData $type | select *

TypeName : System.Management.ManagementObject#root\

cimv2\Win32_OperatingSystem...

Members : {[Installed,

System.Management.Automation.Runspaces.

ScriptPropertyData],

[OperatingSystem,

System.Management.Automation.Runspaces.

AliasPropertyData],

[Computername,

System.Management.Automation.Runspaces.

AliasPropertyData],

[ServicePack,

System.Management.Automation.Runspaces.

AliasPropertyData]...}

TypeConverter :

TypeAdapter :

IsOverride : False

SerializationMethod :

TargetTypeForDeserialization :

SerializationDepth : 0

DefaultDisplayProperty :

InheritPropertySerializationSet : False

StringSerializationSource :

DefaultDisplayPropertySet : System.Management.Automation.Runspaces.

PropertySetData

DefaultKeyPropertySet :

PropertySerializationSet :

You can see the new members you defined. You can also see the new default display property set:

PS C:\> Get-TypeData $type |

>> select -ExpandProperty DefaultDisplayPropertySet

>>

ReferencedProperties

--------------------

{Computername, Operatingsystem, ServicePack, OSArchitecture...}

27.7. Remove-TypeData

If you decide to revert your type extensions, you can use the Remove-TypeData cmdlet. This cmdlet will remove extensions from the current session regardless of whether they were loaded from an XML file or dynamically. Only the extensions in the current session, and no XML files, are deleted. Go ahead and remove the WMI extensions you’ve been testing:

PS C:\> Remove-TypeData $type

You can now continue in your current session and Win32_OperatingSystem instances will be displayed just as before. Well, almost. When you remove the type it also removes the default display property set, so the next time you run a command you’ll get all properties. If you think you might need to revert, save the default display set first:

$dds = Get-TypeData $type |

select -ExpandProperty DefaultDisplayPropertySet |

select -ExpandProperty ReferencedProperties

Then, should you need to reset them, you can do so dynamically:

PS C:\> Update-TypeData -TypeName $type –DefaultDisplayPropertySet $dds

The only way to return to a completely default environment is to start a new PowerShell session, but if you plan ahead it might save some headaches.

27.8. Summary

Type extensions aren’t something every administrator needs to worry about. They’re definitely a specialized piece of PowerShell. But when you run into a situation that calls for them, it’s certainly nice to know how they work. Keep in mind that those XML files are case sensitive! We usually prefer to copy and paste chunks of XML from Microsoft’s types.ps1xml file and then modify our pasted parts to suit our needs. That way, we’re much less likely to mess up the XML. Always, always remember not to modify the default PowerShell type files but create your own and useUpdate-TypeData to load them into memory either from an XML file or dynamically.