PowerShell: Custom Types, and Formatting

So you’ve graduated to the PowerShell elite, you’ve mastered the pipeline, and one-liners seem well trivial.  What now?  For me, I started working on a module, and along the way I was forced to really learn PowerShell’s Type and Formatting subsystems.  As I see advanced functions start to propagate out into the world I wanted to share some of what I learned (As well as provide myself a useful reference guid!).

Why should you care? Well,  in order for your script/module to truly perform like a first class cmdlet.  You must have a custom type.  Having this custom type will cause several built-in automation gremlins to get confused…. I.e. PowerShell doesn’t know how to handle your new object!  With a combination of type, and format data we’ll teach PowerShell how to handle our object. 

First we need a custom object, I’ll use an example slightly above foo.bar..  First things first we need to construct our object, I won’t get too into this as I’m not qualified to speak on the matter.  All I will offer is this I’ve seen examples using c# interfaces as the object construct.  I prefer using a class as it allows me to define multiple Constructors, which become very useful later on in your code!

Add-Type -Language CSharpVersion3  -TypeDefinition @”
using System.Collections;
namespace GetAdmin
{
    public class Net
    {
        #region Paramaters 
 
        public string Interface { get; set; }
        public System.Net.IPAddress IPAddress { get; set; }
        public string Netmask { get; set; } 
 
        #endregion Parameters 
 
        #region Constructors
        public Net()
        {
            Interface = null;
            IPAddress = new System.Net.IPAddress((long)16777343);
            Netmask = null;
        }
        public Net(
            string name,
            System.Net.IPAddress ipaddress,
            string netmask
        )
        {
            Interface = name;
            IPAddress = ipaddress;
            Netmask = netmask;
        }
        #endregion Constructors
    }
    public class Server
    {
        #region Paramaters 
 
        public string    Name    { get; set; }
        public ArrayList Network { get; set; } 
 
        #endregion Parameters 
 
        #region Constructors
        public Server()
        {
            Name    = null;
            Network = new ArrayList();
        }
        public Server(
            string name,
            ArrayList network
        )
        {
            Name    = name;
            Network = network;
        }
        #endregion Constructors
    }
}
“@
[0:2]PS> #Testing our new object
[0:3]PS> New-Object GetAdmin.Net

Interface                               IPAddress                               Netmask
---------                               ---------                               -------
                                        127.0.0.1

[0:4]PS> New-Object GetAdmin.Net("ns0", "192.168.1.1", "255.255.255.0")

Interface                               IPAddress                               Netmask
---------                               ---------                               -------
ns0                                     192.168.1.1                             255.255.255.0

So far so good right?  Well, we already need a little tweaking.  Consider what happens next when we instantiate our Server object.. 

[0:5]PS> New-Object GetAdmin.Server -ArgumentList @(
>>     "Server1",
>>     (1..3 | Foreach {
>>         New-Object GetAdmin.Net("ns0", "192.168.1.$_", "255.255.255.0")
>>     })
>> )
>>

Name                                                        Network
----                                                        -------
Server1                                                     {GetAdmin.Net, GetAdmin.Net, GetAdmin.Net}

Notice how PowerShell just repeated the type name over and over again.  That’s because we haven’t told it what we care about and how to display the information in this situation. I know this is a rare case but it’s one I’ve found all to often!  Fortunately this is handled easily enough. Ultimately we will handle this with a format data file, but first we need to extend our types!  While it is possible to do all this work in the format file, I highly advise against it as it will lead to confusion by your users!

First we’ll create a GetAdmin.Types.ps1xml file and add the following, permanently adding a couple script properties to our objects.

<types>
  <type>
    <name>GetAdmin.Net</name>
    <members>
      <scriptproperty>
        <name>IP</name>
        <getscriptblock>
          $this.IPAddress.tostring()
        </getscriptblock>
      </scriptproperty>
    </members>
  </type>
  <type>
    <name>GetAdmin.Server</name>
    <members>
      <scriptproperty>
        <name>IPAddress</name>
        <getscriptblock>
          ($this.Network | select-object -Expand IP) -join ', '
        </getscriptblock>
      </scriptproperty>
    </members>
  </type>
</types>

As you can see we added a script property to our Net object that will return the the ip in a string.  Then we used that IP property in the IPAddress script property to create a comma separated list of IP’s.  Then we’ll use that list of IP’s as the default view for the network.  This is done with a simple format data file. A quick note with the format data file: it’s a good idea to format to 80chars wide, as this is the default for most 3rd party consoles.

<Configuration>
  <ViewDefinitions>
    <View>
      <Name>Server</Name>
      <ViewSelectedBy>
        <TypeName>GetAdmin.Server</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>Name</Label>
            <Width>20</Width>
            <Alignment>Left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>IPAddress</Label>
            <Width>60</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>Name</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>IPAddress</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

Now when we run our command we’re displaying useful information right from the start, but we haven’t hampered anything.  Remember the full object is still available underneath.

[0:6]PS> Update-FormatData ..\..\..\Documents\GetAdmin.Format.ps1xml
[0:7]PS> Update-TypeData ..\..\..\Documents\GetAdmin.Types.ps1xml
[0:8]PS> New-Object GetAdmin.Server -ArgumentList @(
>>     "Server1",
>>     (1..3 | Foreach {
>>         New-Object GetAdmin.Net("ns0", "192.168.1.$_", "255.255.255.0")
>>     })
>> )
>>

Name                 IPAddress
----                 ---------
Server1              192.168.1.1, 192.168.1.2, 192.168.1.3

That’s all for now, Next time type conversion!

~Glenn Sizemore

Powershell
Scripting

Comments (0)

Permalink

PowerShell: Custom Types, Type conversion and ETS

Before we get into the how, here is why you should care about this!  Consider the following oversimplified function.

Function new-Server {
    <#
    .SYNOPSIS
        Create a new GetAdmin.Server object
    .PARAMETER Name
        Server Name
    .PARAMETER Network
        Detailed per interfave network information
    .EXAMPLE
        Get-NavFiler
    .Outputs
        Netapp.SDK.NavFiler[]
    #>
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName=$TRUE)]
        [string]
        $Name,
 
        [Parameter()]
        [GetAdmin.Net[]]
        $Network
    )
    Process {
        return New-Object GetAdmin.Server -ArgumentList @($Name, $network)
    }
}
First off we need to load our custom types:
Add-Type -Language CSharpVersion3  -TypeDefinition @”
using System.Collections;
namespace GetAdmin
{
    public class Net
    {
        #region Paramaters 
 
        public string Interface { get; set; }
        public System.Net.IPAddress IPAddress { get; set; }
        public string Netmask { get; set; } 
 
        #endregion Parameters
 
        #region Constructors
        public Net()
        {
            Interface = null;
            IPAddress = new System.Net.IPAddress((long)16777343);
            Netmask = null;
        }
        public Net(
            string name,
            System.Net.IPAddress ipaddress,
            string netmask
        )
        {
            Interface = name;
            IPAddress = ipaddress;
            Netmask = netmask;
        }
        #endregion Constructors
    }
    public class Server
    {
        #region Paramaters 
 
        public string    Name    { get; set; }
        public ArrayList Network { get; set; } 
 
        #endregion Parameters
 
        #region Constructors
        public Server()
        {
            Name    = null;
            Network = new ArrayList();
        }
        public Server(
            string name,
            ArrayList network
        )
        {
            Name    = name;
            Network = network;
        }
        #endregion Constructors
    }
}
“@

Now let try and use this function:

[0:3]PS>$Network = 1..3 | Foreach {
    New-Object GetAdmin.Net("ns0", "192.168.1.$_", "255.255.255.0")
}
[0:4]PS> $network

Interface                               IPAddress                               Netmask
---------                               ---------                               -------
ns0                                     192.168.1.1                             255.255.255.0
ns0                                     192.168.1.2                             255.255.255.0
ns0                                     192.168.1.3                             255.255.255.0

[0:5]PS> New-Server -Name Server1 -Network $Network

Name                                                        Network
----                                                        -------
Server1                                                     {192.168.1.1, 192.168.1.2, 192.168.1.3}

Why that’s not that bad, consider the following, it could look like this.

[0:4]PS>New-Server –Name Server1 –Network "ns0,192.168.1.1,255.255.255.0", "ns0,192.168.1.2,255.255
.255.0", "ns0,192.168.1.3,255.255.255.0"

Name                                                        Network
----                                                        -------
Server1                                                     {192.168.1.1, 192.168.1.2, 192.168.1.3}


Now I really don’t want to get into which one you should be using.  I prefer instead to focus on how to enable either one!

First things first we’re going to have to get a little dirty with some c# (Again I’m not a developer so if anyone knows a better way to do this I’m all ears.) and create a type converter.

Add-Type -Language CSharpVersion3  -TypeDefinition @”
using System;
using System.Collections;
using System.Management.Automation;
 
namespace GetAdmin
{
    public class NetConverter : PSTypeConverter
    {
        /// Override for the CanConvertFrom Method.
        /// Returns true if the Source object
        /// is of type String and can be Converted to GetAdmin.Net type
        public override bool CanConvertFrom(Object sourceValue, Type destinationType)
        {
            string src = sourceValue as string;
            if (src != null)
            {
                try
                {
                    string[] Fields = src.Split(new char[1] { ',' });
                    if (Fields.GetLength(0) == 3)
                    {
                        return true;
                    }
                }
                catch (Exception)
                {
                    return false;
                }
            }
            return false;
        }
 
        /// Override for the ConvertFrom Method
        public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider provider, bool IgnoreCase)
        {
            if (sourceValue == null)
                throw new InvalidCastException("no conversion possible");
            if (this.CanConvertFrom(sourceValue, destinationType))
            {
                try
                {
                    // Cast our input as a string just in case
                    string src = sourceValue as string;
                    // create a new GetAdmin.Net object
                    GetAdmin.Net n = new GetAdmin.Net();
 
                    if (src != null || src != "")
                    {
                        //split our string into an array using commas as the delim
                        string[] Fields = src.Split(new char[1] { ',' });
 
                        //populate our new object
                        n.Interface  = Fields[0];
                        n.IPAddress  = System.Net.IPAddress.Parse(Fields[1]);
                        n.Netmask    = Fields[2];
                    }
                    //return our new GetAdmin.Net Object
                    return n;
                }
                catch (Exception)
                {
                    throw new InvalidCastException("no conversion possible");
 
                }
            }
            throw new InvalidCastException("no conversion possible");
        }
        /// Default to PowerShell conversion for other types.
        /// Return False here
        public override bool CanConvertTo(object Value, Type destinationType)
        {
            return false;
        }
        /// Do not handle conversion for other types
        public override object ConvertTo(object Value, Type destinationType,
        IFormatProvider provider, bool IgnoreCase)
        {
            throw new InvalidCastException("conversion failed");
        }
    }
    public class Net
    {
        #region Paramaters 
 
        public string Interface { get; set; }
        public System.Net.IPAddress IPAddress { get; set; }
        public string Netmask { get; set; } 
 
        #endregion Parameters
 
        #region Constructors
        public Net()
        {
            Interface = null;
            IPAddress = new System.Net.IPAddress((long)16777343);
            Netmask = null;
        }
        public Net(
            string name,
            System.Net.IPAddress ipaddress,
            string netmask
        )
        {
            Interface = name;
            IPAddress = ipaddress;
            Netmask = netmask;
        }
        #endregion Constructors
    }
    public class Server
    {
        #region Paramaters 
 
        public string    Name    { get; set; }
        public ArrayList Network { get; set; } 
 
        #endregion Parameters
 
        #region Constructors
        public Server()
        {
            Name    = null;
            Network = new ArrayList();
        }
        public Server(
            string name,
            ArrayList network
        )
        {
            Name    = name;
            Network = network;
        }
        #endregion Constructors
    }
}
“@

I told you we needed to get a little dirty!  Now that we’ve implemented a type converter that knows how to convert a string to a GetAdmin.Net object.  Now we need to inform PowerShell of our work.  This is accomplished via the a type formatting file.

<Types>
  <Type>
    <Name>GetAdmin.Net</Name>
    <TypeConverter>
      <TypeName>GetAdmin.NetConverter</TypeName>
    </TypeConverter>
  </Type>
</Types>

Next import your new type definition file, and try it out!

[0:1]PS> Update-FormatData ..\..\..\Documents\GetAdmin.Format.ps1xml
[0:2]PS> Update-TypeData ..\..\..\Documents\GetAdmin.Types.ps1xml
[0:3]PS> [GetAdmin.Net[]]("eth0,192.168.1.1,255.255.255.0","eth1,192.168.2.2,255.255.255.0")

Interface                               IPAddress                               Netmask
---------                               ---------                               -------
eth0                                    192.168.1.1                             255.255.255.0
eth1                                    192.168.2.2                             255.255.255.0
[0:3]PS> New-Server –Name Server1 –Network "ns0,192.168.1.1,255.255.255.0", "ns0,192.168.1.2,255.255
.255.0", "ns0,192.168.1.3,255.255.255.0"

Name                                                        Network
----                                                        -------
Server1                                                     {192.168.1.1, 192.168.1.2, 192.168.1.3}

Hope that was helpful, know a better way?

~Glenn Sizemore

Powershell
Scripting

Comments (2)

Permalink