Windows PowerShell is a command-shell environment that supports object-oriented scripting based on the Microsoft .NET Framework. At its heart is the PowerShell cmdlet, a lightweight command that performs a specific action and, in the process, returns a .NET object. PowerShell lets you use cmdlets to access a variety of systems and perform numerous tasks against those systems. However, the cmdlets alone aren't always enough to carry out certain actions. For those actions, you must often create your own objects based on the .NET classes. One type of .NET object you can build is the custom object. A custom object lets you define the types of data assigned to that object and the actions it can carry out. After you understand how to build a custom object, you'll have a powerful tool for building scripts that can extend PowerShell's already significant reach.

Note that the .NET Framework and object-oriented programming are complex topics, well beyond the scope here. To fully appreciate PowerShell's object-oriented nature, it can help to have at least a basic understanding of these concepts. For more information about the .NET Framework, see the MSDN article "Overview of the .NET Framework." For an introduction to object-oriented programming, see the MSDN article "Object-Oriented Programming (C# and Visual Basic)."

Creating a Custom Object

To create a custom object in PowerShell, you must first use the New-Object cmdlet to build the initial object. This cmdlet lets you create .NET or COM objects from an assortment of classes. A custom object is a special type of .NET object based on either the Object or PSObject .NET class. There's no difference between the two when creating the initial object. However, the PSObject class is generally preferred because of issues that can arise when adding properties to an object based on the Object class, so let's use PSObject.

To create an object based on the PSObject class, you need to use the New-Object cmdlet with the -TypeName parameter and specify the PSObject class as the parameter's value, like so:

$system = New-Object -TypeName PSObject

When you run this command, it will generate a PSCustomObject object and assign it to the $system variable. More often than not, you'll want to assign the object to a variable so that you can easily work with that object's members. The members are the components that make up an object, such as properties, methods, alias properties, and member sets.

When working with objects in PowerShell, the two member types you'll likely use are properties and methods. A property describes a specific aspect of the object. For example, one of the members of the FileInfo .NET class is the Length property, which provides a file's size in bytes. If you create a FileInfo object based on that class, you can use the Length property to access the size of the file associated with that object. A method carries out an action related to the object. For example, the FileInfo class also includes the Delete method, which you can use to delete the file represented by the object.

Being able to easily identify an object's members makes it easier to work with that object. To get a list of the membersthat make up an object, you can use the Get-Member cmdlet. For example, to look at the members of the new object that was assigned to the $system variable, you can pipe the $system variable to the Get-Member cmdlet:

$system | Get-Member

As Figure 1 shows, the object you've created is based on the System.Management.Automation.PSCustomObject class, which includes only a small number of members, all of which are methods. But that's why it's called a custom object—you get to customize it by adding your own members.

Figure 1: The New Object's List of Members

Before I show you how to add members, let me just add that, like many .NET classes, the PSCustomObject class also includes a number of hidden members of various types, such as memberset and codeproperty. You can view those members by including the -Force parameter when calling the Get-Member cmdlet. Although a discussion of hidden members is beyond the scope here, you might find it useful at some point to explore the hidden members available to the PSCustomObject class and other .NET classes to take full advantage of your PowerShell objects.

Adding Properties to a Custom Object

A custom object is only as useful as the members associated with the object. You can add several different types of members to a custom object. For this example, you'll be adding the NoteProperty and ScriptMethod members. A NoteProperty member is similar to a regular property, and a ScriptMethod member is much like a regular method. You can't add a regular property or method to a custom object, which is why you're going to use NoteProperty and ScriptMethod. (For information about the supported members in a custom object, see the PowerShell TechNet topic "Add-Member.")

Let's start with adding a couple of NoteProperty members to the $system object. To add the members, you can use the Add-Member cmdlet to define the property names and values. For instance, suppose you want to add note properties based on data retrieved through the Windows Management Instrumentation (WMI) capabilities built into PowerShell. (WMI provides an infrastructure for managing data and operations in Windows OSs.) The WMI object model includes numerous classes that support a wide range of management operations. For example, the Win32_OperatingSystem class provides access to such information as the name of the OS installed on a computer and the latest service pack applied to that OS.

To create a WMI object, you use the Get-WmiObject cmdlet and specify the desired class. For example, to create a Win32_OperatingSystem WMI object and assign it to the $os variable, you'd use the code:

$os = Get-WmiObject Win32_OperatingSystem

In this case, you're retrieving system information from the local computer, but the beauty of WMI is its ability to also access and manage remote Windows computers.

You can then use the $os variable to access the information returned by that object. Assigning an object to a variable this way locks the data at that point in time. For example, if a new service pack is applied to the OS after you create the $os variable, the object's CSDVersion property won't reflect the change because the object had been created based on the original data.

After defining the $os variable, you need to specify two Add-Member commands. Each command will take four parameters:

  • -InputObject. You use the -InputObject parameter to identify the object receiving the new note properties. In this case, you need to specify the $system object.
  • -MemberType. The -MemberType parameter indicates the type of member to create, so you need to specify NoteProperty.
  • -Name. You use the -Name parameter to specify the name of the new member. For this example, let's name the first member OperatingSystem and the second member ServicePack.
  • -Value. This parameter specifies the member's value. For the first note property, you need to specify $os.Caption to return the name of the OS. For the second note property, you need to specify $os.CSDVersion to return the latest service pack applied to the OS.

Here is what the two Add-Member commands look like:

Add-Member -InputObject $system -MemberType NoteProperty `
  -Name OperatingSystem -Value $os.Caption
Add-Member -InputObject $system -MemberType NoteProperty `
  -Name ServicePack -Value $os.CSDVersion

Notice that I've broken each Add-Member command into two lines. At the end of each first line, I included a back tick (`) so that the PowerShell processor knows to continue to the next line for the complete command.

After you've run the Add-Member commands, you can call the $system variable to view its contents:

$system

Figure 2 shows sample results.

Figure 2: Contents of the $system Variable

You can also use the $system variable to call individual members. Simply specify the variable name, followed by a period, then the member name. For example, the following command returns the current value of the OperatingSystem note property:

$system.OperatingSystem

One advantage of creating a custom object is that it lets you create note properties based on data that comes from different sources as well as a note property whose value is a specific string. For example, take a look at the code in Listing 1, which adds three new note properties to the $system object.

Listing 1: Code That Uses the Add-Member Cmdlet to Add Three Note Properties
$mem = Get-WmiObject Win32_PhysicalMemory
$disk = Get-WmiObject Win32_DiskDrive

Add-Member -InputObject $system -MemberType NoteProperty `
  -Name PhysicalMemory `
  -Value (("{0:N2}" -f ($mem.Capacity/1GB)) + ' GB')
Add-Member -InputObject $system -MemberType NoteProperty `
  -Name DiskSize `
  -Value (("{0:N2}" -f ($disk.Size/1GB)) + ' GB')
Add-Member -InputObject $system -MemberType NoteProperty `
  -Name Owner -Value "janetp"

Most of the elements in Listing 1 you saw in the previous example. However, notice that the code is creating an instance of the Win32_PhysicalMemory class, which is assigned to the $mem variable, and an instance of the Win32_DiskDrive class, which is assigned to the $disk variable. Two of the added note properties use these new WMI objects as data sources.

The first Add-Member command retrieves data from the $mem.Capacity property, which provides the memory's capacity on the target system. In this case, however, the -Value parameter is a bit more complex than you saw in the previous example. First, the command specifies "{0:N2}" followed by -f, which is a .NET construction for formatting the outputted number. Basically, this specifies that a number be returned with two decimal places. The formatting instructions are followed by a short expression ($mem.Capacity/1GB) that divides the Capacity value by 1GB. The Capacity value provides the size of the memory in bytes. PowerShell makes it easy to convert bytes to gigabytes simply by dividing by 1GB. Finally, the command tags the string ' GB' (including the space) onto the output and encloses the whole thing in parentheses.

The second Add-Member command follows the same logic to return the size of the computer's hard disk. It uses the $disk.Size property to return the size in bytes, then converts it to gigabytes.

The third Add-Member command adds a note property named Owner and assigns it the string value janetp. In this case, "owner" can mean anything—the primary user, the last user, the administrator, or whatever meaning you want to impose on the property. I included it here only to demonstrate that you can add a note property whose value is a simple string.

After you've added the three members to the custom object, you can once again call the $system variable. It will return results like those shown in Figure 3.

Figure 3: Note Properties Added with the Add-Member Cmdlet

You now have a single object that contains data derived from three different WMI objects. The object also contains the Owner property, which is based on a string value. You can call that note property as you would any other member:

$system.Owner

As you'd expect, the command returns the value janetp. However, you can change that value to a different one by assigning a new value to the property, like so:

$system.Owner = "rogert"

In this case, the value rogert has been assigned to the Owner note property. If you call that note property now, it will return the new value.

Like with many of the tasks you can perform in PowerShell, there are a number of different ways to add a member to a custom object. For example, PowerShell 3.0 lets you create a hash table and pass the hash to the New-Object command. The code in Listing 2 creates the same five note properties but uses a hash table to pass in the member names and values.

Listing 2: Code That Uses a Hash Table to Create the Five Note Properties
$info = @{
  "OperatingSystem" = $os.Caption;
  "ServicePack" = $os.CSDVersion;
  "PhysicalMemory" =
    (("{0:N2}" -f ($mem.Capacity/1GB)) + ' GB');
  "DiskSize" = (("{0:N2}" -f ($disk.Size/1GB)) + ' GB');
  "Owner" = 'janetp'
}

$system = New-Object -TypeName PSObject -Property $info

$system

This code first creates a hash table that defines five property/value pairs, each corresponding to the note properties. As the code shows, you need to separate the property/value pairs with semicolons. You also need to enclose the set of five property/value pairs in curly brackets and precede the opening bracket with the at (@) symbol. Finally, you need to assign the hash table to the $info variable.

After defining the hash table, the code uses the New-Object cmdlet to create an object based on the PSObject class and assigns it to the $system variable. Only this time, the command includes the -Property parameter, which takes the $info variable as its value.

Finally, the code calls the $system variable, which returns results like that in Figure 4. Notice the order in which the properties have been added to the object. One of the problems with using a hash table to add note properties is that you can't control their order. To control the order, you must use the Add-Member cmdlet to add each member.

Figure 4: Note Properties Created with the Hash Table

Adding Methods to a Custom Object

Now that you've seen how easy it is to add properties, access their values, and change those values, let's move on to methods. These get a bit more complicated because methods have to do something, and you need to define that something as part of the method's value.

For example, the code in Listing 3 creates a method that retrieves the current date and time and returns them in both their original format and as Coordinated Universal Time (UTC) values.

Listing 3: Code That Creates a Method
$method =
{
  $a = Get-Date -Format F; "Local: " + $a;
  $b = Get-Date; "UTC: " + $b.ToUniversalTime().DateTime
}

Add-Member -InputObject $system -MemberType ScriptMethod `
  -Name GetUTC -Value $method

To keep the code readable, you can assign the method's value to a variable (e.g., $method). That value includes the expressions necessary to return the date and time information. However, you must define the value as a script block in order to pass it into the Add-Member command. To do so, you enclose the value's contents in curly brackets. PowerShell will then assign the value in its current form to the variable as a ScriptBlock object.

In Listing 3, the method's value includes two expressions. The first expression returns the current date and time, and the second expression returns the UTC version. You start the first expression by using the Get-Date cmdlet to retrieve the current date and time, which you assign to the $a variable. Note that when you call the Get-Date cmdlet, you need to include the -Format parameter, along with the value F. This tells PowerShell to display the timestamp in a full date and time format, as in Monday, August 19, 2013 12:28:25 PM. You then add the string "Local: " (including the space) to the variable value to return the current date and time along with that label.

In the second expression, you again start with the Get-Date cmdlet to return the current date and time, which you assign to the $b variable. You then use the variable's ToUniversalTime method to return the date and time as a UTC value, then specify the DateTime property to return the correct format.

If you were to call the $method variable at this point, it would return the two expressions as strings, just as they were typed. Because you defined the variable value as a script block, the commands aren't processed when they're assigned to the $method variable. Instead, the $method variable is created as a ScriptBlock object, which you can confirm by using the GetType method to see the variable's type:

$method.GetType()

To work with the script block, you need to add the method as a member of the $system object after defining the $method variable. To do so, you create an Add-Member command that specifies ScriptMethod as the type, GetUTC as the name, and the $method variable as the value.

After running the code in Listing 3, you can use the $system variable to call the GetUTC method in much the same way you called the properties:

$system.GetUTC()

Notice, however, that when you call the script method, you must include the parentheses, just like you do for any other method, even if you're not passing in arguments. The method should now return results similar to those in Figure 5 but with the date and time when you called the method.

Figure 5: Sample Results from the GetUTC() Method

In real life, you'll probably want to create methods that pack more punch. For example, the $system object might incorporate WMI management tasks in its script methods. If you do so, you need to fully test the method to make sure your commands are doing exactly what they're supposed to do. PowerShell and WMI are both powerful tools, so they should be used with care when managing Windows computers.

Adding Custom Objects to an Array

Custom objects aren't limited to standalone objects that you use one time and toss aside. You can add those objects to an object array, then access the objects through that array. For example, suppose you want to manipulate information you retrieve from multiple computers. That information might include details about the memory and logical drives available on those systems. You want to be able to work with those details in a meaningful way in PowerShell. Because of PowerShell's object-oriented nature, putting that information into objects is often the simplest way to work with that data.

Let's look at an example of how you might do this. For this discussion, the approach used to collect the information about the various computers is unimportant. You might, for example, set up a foreach loop in PowerShell that uses WMI to retrieve the information from the different workstations. Regardless of the method used, for the sake of brevity, assume that you've collected the information into a comma-separated text file that contains the data shown in Figure 6.

Figure 6: Collected WMI Data

The text file includes a header row and a row for each system's logical drives. For example, the first row of data provides details about the C logical drive on computer ws01. The logical drive has a capacity of 500GB, with 225GB of free space. The system also has 4GB of memory. The second row indicates that the same computer also contains the D logical drive, which has the same capacity as the C drive but with 320GB of free space.

The goal is to turn each row of data into its own custom object, then add those objects to an array. You can then take action on the objects within the array by piping the data to other PowerShell commands. The code in Listing 4 defines an empty array, imports the data from the text file, then uses a foreach loop to create the objects and add them to the array.

Listing 4: Code That Turns Rows of Data into Custom Objects and Adds Them to an Array
$SystemInfo = @()

$SourceData = Import-CSV C:\DataFiles\SourceData.txt

foreach ($source in $SourceData)
{
  $system = New-Object -TypeName PSObject
  $system | Add-Member -Type NoteProperty `
    -Name Computer -Value $source.Computer
  $system | Add-Member -Type NoteProperty `
    -Name DeviceID -Value $source.DeviceID
  $system | Add-Member -Type NoteProperty `
    -Name DriveSize -Value ([int]$source.DriveSize)
  $system | Add-Member -Type NoteProperty `
    -Name UsedSpace `
    -Value ([int]$source.DriveSize - $Source.FreeSpace)
  $system | Add-Member -Type NoteProperty `
    -Name FreeSpace -Value ([int]$source.FreeSpace)

  $SystemInfo += $system
}

As you can see, the first step is to create the empty array (@()) and assign it to the $SystemInfo variable. Eventually, you'll add the custom objects to this array.

Next, you use the Import-CSV cmdlet to import the data into the $SourceData variable. For this exercise, the text file is named SourceData.txt and saved to the C:\DataFiles folder. The Import-CSV cmdlet retrieves each row as its own object, saving it to $SourceData. At this point, you could use that variable to access the data, but you'd have no control over the members included in the object, nor would you be able to add members.

The next step, then, is to create a foreach loop that lets you create a custom object for each row in the text file. The foreach condition ($source in $SourceData) specifies that each row in $SourceData should be assigned to the $source variable as the code iterates through the foreach loop. The logic defined within the foreach script block (enclosed in curly brackets) will then run one time for each row, with a different $source object being applied during each iteration. (For more information about creating a foreach loop, see the PowerShell TechNet topic about_ForEach.)

Let's take a closer look at the foreach script block. The first command creates the initial object and saves it to the $system variable, as you saw in previous examples. Next, the $system variable is piped to an Add-Member command in order to create the first note property. This is a slightly different format than used before. Instead of including the -InputObject parameter in the Add-Member command, you simply pipe $system to the command. This achieves the same results you saw in the earlier examples of Add-Member commands. I've taken this approach here simply to demonstrate one more way that PowerShell lets you add members.

For the Add-Member command's value, you use the $source variable to call the Computer property, which corresponds to the Computer field in the source data. The second Add-Member command works the same way, except that it retrieves data from the DeviceID property. Notice that the Memory property isn't included. When you're creating a custom object, you can omit specific properties or put them in a different order.

The third Add-Member command works much the same way as the first two, except that it's accessing the DriveSize property. Because you might want to work with this data as a numerical value rather than a string, the command explicitly converts the data to a number. This is achieved by preceding the property name with [int] to indicate that you want theint data type, then enclosing the entire property expression in parentheses.

The fourth Add-Member command does something a little different. It creates a calculated member that subtracts the FreeSpace value from the DriveSize value in order to determine the amount of used space. Again, the property name is preceded with [int] and the entire expression is enclosed in parentheses. Being able to create a calculated property is one of the main reasons why you might want to create custom objects.

The final Add-Member command works just like the third, except it retrieves data from the FreeSpace property.

The final command in the foreach script block adds the current $system object to the $SystemInfo array. Notice that the += operator is used to ensure that the new $system object is added with each iteration of the loop, without anything being overwritten.

After you run the code in Listing 4, you can call the variable to view its contents. Figure 7 shows a partial list of the data now stored in the $SystemInfo array.

Figure 7: Data Stored in the $SystemInfo Array

Specifically, it shows the data for the first seven objects in the array, which corresponds to the first seven rows of data in the original text file. Each grouping represents one of the objects added to the $SystemInfo array. You can verify that you created an object array by using the GetType method to retrieve the variable's type:

$SystemInfo.GetType()

If this command returns a BaseType of System.Array, the $SystemInfo variable is indeed an object array capable of holding custom objects. You can then use that variable to manipulate the data however you see fit. For example, you can pipe the variable to a Sort-Object command to sort the data in descending order, based on the FreeSpace values:

$SystemInfo | Sort FreeSpace -Descending

In this case, the command uses the Sort alias to reference the Sort-Object cmdlet and includes the FreeSpace property and the -Descending parameter. As you can see in Figure 8, the objects are now listed according to the FreeSpace values, with the highest amount of free space listed first. Once again, this is only a partial list.

Figure 8: Data Sorted by the Amount of Free Space

You can also sort the data based on multiple columns. For example, you might sort the data first by the Computer property, then the FreeSpace property, in descending order:

$SystemInfo | Sort Computer, FreeSpace -Descending

As you'd expect, the results are now much different, as Figure 9 shows.

Figure 9: Data Sorted by the Computer Name and the Amount of Free Space in Descending Order

One of the consequences of sorting the columns in this way is that the Computer values as well as the FreeSpace values are sorted in descending order. The Sort-Object cmdlet doesn't let you easily sort one column in ascending order and another one in descending order. If you specify the -Descending parameter, all data is sorted in that order. However, you can work around this limitation by creating expressions for each property that specify how to sort those values. For example, the following command sorts first by the Computer property in ascending order, then by the FreeSpace property in descending order:

$SystemInfo | Sort `
  @{Expression="Computer"; Descending=$false},
  @{Expression="FreeSpace"; Descending=$true}

The first Sort-Object expression sets the Expression property to Computer and the Descending property to $false. The second expression sets the Expression property to FreeSpace and the Descending property to $true. (The $true and $false variables are built-in system variables that provide the true and false Boolean values of 1 and 0, respectively.) You must then enclose each expression in curly brackets and precede each opening bracket with the at (@) symbol. Now the data will be sorted in the desired order, as shown in Figure 10.

Figure 10: Data Sorted by the Computer Name in Ascending Order and the Amount of Free Space in Descending Order

This might seem like overkill on the topic of sorting, but it points to the flexibility of working with objects in an array. And you're certainly not limited to the Sort-Object cmdlet. For example, the following command pipes the $SystemInfo variable to the Where-Object cmdlet:

$SystemInfo | Where DriveSize -gt 250 |
  Sort FreeSpace -Descending

In this case, the command uses the Where alias to reference the Where-Object cmdlet. In addition, it's specifying that the DriveSize value must be greater than 250 to be included in the results. (In PowerShell, -gt is used for the greater than operator.) The results meeting those criteria are piped to the Sort-Object cmdlet so that the data is displayed in the proper order.

You can even pipe the sorted data to a Select-Object command, like this:

$SystemInfo | Where DriveSize -gt 250 |
  Sort FreeSpace -Descending | Select -First 5

This command uses the Select-Object cmdlet (referenced by the Select alias) to return only the first five rows of the result set. As you can see, once you add custom objects to the $SystemInfo array, you have a variety of options for working with those objects.

Making the Most of Your Custom Objects

For the most part, creating custom objects in PowerShell is a fairly straightforward process. If there's any complexity involved, it generally comes from the value expressions you define for your note properties or script methods. The expressions in these examples are relatively simple compared to the types of expressions you can create in PowerShell. Even so, what you've seen here should provide you with the foundation you need for creating custom objects. When used effectively, they can be a powerful component of your PowerShell scripts, whether you create individual objects, add them to an array, or use them in some other way. You might, for example, create custom objects within your PowerShell profile so they're available each time you start a new session. How you use custom objects in your scripts is up to you. Just know that the custom object is an effective, flexible tool that can serve many of your scripting needs.

For additional information on creating custom objects in Windows PowerShell, see Bill Stewart's "Creating Custom Objects in Windows PowerShell."