PowerShell in Depth, Second Edition (2015)
Part 3. PowerShell scripting and automation
Chapter 21. Creating objects for output
This chapter covers
· “Objectifying” your output
· Creating custom objects
· Working with collections of properties
In the previous chapter, we showed you how to create a simple script and turn it into a function. We emphasized the need for scripts and functions to output only one kind of thing, and in those simple examples you found it easy to comply because you were running only a single command. But you’re doubtless going to come across situations where you need to run multiple commands, combine pieces of their output, and use that as the output of your script or function. This chapter will show you how: The goal is to create a custom object that consolidates the information you need and then output it from your script or function. Richard remembers being asked at a conference session if PowerShell had a command that worked in a similar way to the Union command in SQL. This chapter is the closest you’ll get with PowerShell because you’re working with objects.
21.1. Why output objects?
Objects are the only thing a script (or function; from here out you can assume that everything we say applies to both scripts and functions) can output. You may only need to output a simple Boolean True/False value, but in PowerShell that’s a Boolean object. A date and a time? Object. A simple string of characters? Object. More complex data, like the details of a process or a service, are all represented as objects.
Note
We still see lots of people outputting text from their scripts. Simple word of advice. Don’t. You should always output objects. If you output text in a script, get stuck and ask for help in the forums—expect to be told about outputting objects.
Objects, for our purposes, are just a kind of data structure that PowerShell understands and can work with. Developers probably won’t like that definition, so it’s probably best if we don’t tell them.
Creating a custom object allows you to follow the main rule of scripts (and functions), which is to output only one kind of thing—for example, multiple calls to WMI to access several classes. When you need to output information that comes from multiple places, you can create a custom object to hold that information so that your script is outputting just one kind of thing—that custom object. The alternative is to accept that your script is for reporting purposes only and that you won’t ever want to do anything else with the output. This is only acceptable in restricted circumstances, and even Richard is moving away from the concept.
We’re going to start with the four commands shown in listing 21.1. Each retrieves a different piece of data from a computer (we’ll stick with localhost for this example because it’s easy and should work on any computer).
Tip
If you’re creating functions that have a computer name as a parameter, use $env:COMPUTERNAME as the default rather than localhost or “.”. There are a few occasions where the actual name of the machine is required, which you can access through the environment variable and save extra steps in your code.
You don’t want to output all that information, though, so store the retrieved data in a variable. That way, it’ll be easier to extract the pieces you do want. Listing 21.1 has the four commands—keep in mind that running this as is won’t produce any output because you’re storing the objects produced by these commands in variables but you’re not outputting those variables.
Listing 21.1. Starting commands
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
The last of our four commands is slightly different. Although the first three are retrieving things that, by definition, exist only once per computer (operating system, computer system, and BIOS), the last one is retrieving something that a computer often has more than one of (processors). Because all processors in a system will be the same, you’ve elected to just retrieve the first one by piping the command to Select-Object –First 1. Windows Server 2003 and Windows XP will return one instance of the Win32_Processor class per core, so be aware that the results of using that class will vary depending on operating system version.
Note
There’s a hotfix available to resolve this issue for Windows Server 2003 at http://support.microsoft.com/kb/932370—though given the limited time left in the support lifecycle for these products, it may not be worthwhile applying it.
That way, each of our four variables has just one object. That’s important for the next technique we’ll cover. Generally, you’ll want to have your variables contain just one thing if possible.
Tip
In PowerShell v3 and v4 you have the option to use the Common Information Model (CIM) cmdlets in place of the WMI cmdlets. The choice of which to use doesn’t affect the discussion in this chapter. Alternative listings using the CIM cmdlets will be available in the code download.
With your four variables populated, you’re ready to begin putting them in a custom object.
21.2. Syntax for creating custom objects
We’ve often said that there are always multiple ways to do anything in PowerShell, and that’s certainly true for custom objects. We’ll show you all the major ways because you’re likely to run into them in the wild, and we want you to be able to recognize them and use them when you do.
21.2.1. Technique 1: using a hash table
Let’s start with the way that we prefer ourselves. Call it the official way, if you like: We use it because it’s concise, fairly easy to read, and gets the job done. It’s in listing 21.2.
Note
In each of the upcoming listings, we’ll repeat the four original commands from listing 21.1. That way, each of these is a complete, stand-alone example that you can run to see the results.
Listing 21.2. Creating objects using a hash table
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
$props = @{OSVersion=$os.version
Model=$cs.model
Manufacturer=$cs.manufacturer
BIOSSerial=$bios.serialnumber
ComputerName=$os.CSName
OSArchitecture=$os.osarchitecture
ProcArchitecture=$proc.addresswidth}
$obj = New-Object –TypeName PSObject –Property $props
Write-Output $obj
Run the code in listing 21.2 and you’ll see something like this:
Manufacturer : Microsoft Corporation
OSVersion : 6.3.9600
OSArchitecture : 64-bit
BIOSSerial : 036685734653
ComputerName : RSSURFACEPRO2
Model : Surface Pro 2
ProcArchitecture : 64
Because your output included more than four properties, PowerShell chose a list-style layout; you could’ve run Write-Output $obj | Format-Table to force a table-style layout, but the point is that you’ve created a single, consolidated object by combining information from four different places. You did that by creating a hash table, in which your desired property names were the keys and the contents of those properties were the values. That’s what each of these lines did:
Manufacturer=$cs.manufacturer
If you put the hash table entries all on one line, you’ll need to separate each property with a semicolon. If you put each property on a separate line, you don’t need the semicolon, which makes things a little easier to read. The bracketed structure was preceded by an @ sign—telling PowerShell that this is a hash table—and then assigned to the variable $props so that you could easily pass it to the –Property parameter of New-Object. The object type—PSObject—is one provided by PowerShell specifically for this purpose. As an aside, CSName is a WMI property that’s available on the objects returned by Win32_OperatingSystem using both Get-WmiObject and Get-CimInstance.
Tip
The property __SERVER and other system variables aren’t available when using the CIM cmdlets, so we recommend that you don’t use them in your scripts. That way, changing to using the CIM cmdlets will be easier if you decide to do so.
The benefit of this approach is that it’s easy to build a hash table on the fly and create as many custom objects as you need. You’ll also notice that the hash table output isn’t in the same order in which it was defined. One solution is to create a custom type and format extension, which we cover elsewhere in the book. Or in PowerShell v3 and v4, you can create an ordered hash table:
$props = [ordered]@{ OSVersion=$os.version
Model=$cs.model
Manufacturer=$cs.manufacturer
BIOSSerial=$bios.serialnumber
ComputerName=$os.CSName
OSArchitecture=$os.osarchitecture
ProcArchitecture=$proc.addresswidth}
Everything else is the same, but now the object will be displayed with the properties in entered order. If you pipe $obj to Get-Member, you’ll see that the type is a PS-CustomObject.
Note
PowerShell doesn’t by default keep track of the order of items in the hash table. That’s why, when you see the final output object, its properties aren’t in the same order in which you put them in. Beginning with PowerShell v3, you can remedy that by preceding the hash table declaration with the [ordered] attribute as you did earlier. This creates an ordered dictionary (or ordered hash table, if you prefer) and maintains the order of the items.
21.2.2. Technique 2: using Select-Object
This next technique was a favorite in PowerShell v1, and you still see people using it quite a bit. We don’t like it as much as Technique 1 because it can be a bit harder to read. The following listing shows the technique, where you’re basically creating an object that has a bunch of blank properties and then filling in those properties’ values in subsequent steps.
Listing 21.3. Creating objects using Select-Object
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
$obj = 1 | Select-Object ComputerName,OSVersion,OSArchitecture,
ProcArchitecture,Model,Manufacturer,BIOSSerial
$obj.ComputerName = $os.CSName
$obj.OSVersion = $os.version
$obj.OSArchitecture = $os.osarchitecture
$obj.ProcArchitecture = $proc.addresswidth
$obj.BIOSSerial = $bios.serialnumber
$obj.Model = $cs.model
$obj.Manufacturer = $cs.manufacturer
Write-Output $obj
Note that in listing 21.3 the initial $obj = 1 is essentially bogus; the value 1 won’t ever be seen.
Tip
You’ll see many examples where an empty string is used as the starting point: $obj = "" | select .... The same comments apply.
It’s just a way to define $obj as an object so that there’s something in the pipeline to pass to Select-Object, which does all the work.
There’s a potential drawback with this approach. If you pipe $obj to Get-Member, look at the result:
PS C:\> $obj | Get-Member
TypeName: Selected.System.Int32
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
BIOSSerial NoteProperty System.String BIOSSerial=
ComputerName NoteProperty System.String ComputerName= WIN-KNBA0R0TM23
Manufacturer NoteProperty System.String Manufacturer= VMware, Inc.
Model NoteProperty System.String Model= VMware Virtual Platform
OSArchitecture NoteProperty System.String OSArchitecture= 64-bit
OSVersion NoteProperty System.String OSVersion= 6.1.7601
ProcArchitecture NoteProperty System.UInt16 ProcArchitecture=64
Sure the properties are okay, but the typename could lead to problems, or even just confusion, depending on what else you might want to do with this object. We recommend avoiding this technique.
21.2.3. Technique 3: using Add-Member
This technique is what we think of as the formal technique for creating a custom object. Under the hood, this is what happens (more or less) with all the other techniques, so this is a fully spelled-out kind of approach. This approach is more computationally expensive, meaning it’s slower, so you don’t often see folks using it in the real world. Again, this was a more common approach in PowerShell v1. There are two variations, and the following listing has the first.
Listing 21.4. Creating objects using Add-Member
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
$obj = New-Object –TypeName PSObject
$obj | Add-Member NoteProperty ComputerName $os.CSName
$obj | Add-Member NoteProperty OSVersion $os.version
$obj | Add-Member NoteProperty OSArchitecture $os.osarchitecture
$obj | Add-Member NoteProperty ProcArchitecture $proc.addresswidth
$obj | Add-Member NoteProperty BIOSSerial $bios.serialnumber
$obj | Add-Member NoteProperty Model $cs.model
$obj | Add-Member NoteProperty Manufacturer $cs.manufacturer
Write-Output $obj
With the technique shown in listing 21.4, you’re still creating a PSObject but you’re adding one property to it at a time. Each time, you add a NoteProperty, which is the type of property that just contains a static value. That’s exactly what the previous techniques did, but they sort of did it implicitly, under the hood, whereas you’re spelling it out here.
We’re using positional parameters to reduce the amount of code displayed in the listing. Each of the Add-Member statements looks like this when expanded:
Add-Member -MemberType NoteProperty -Name ComputerName -Value $os.CSName
The variation on this technique is to use the –PassThru (abbreviated to –pass in listing 21.5) parameter of Add-Member. Doing so puts the modified object back into the pipeline, so you can pipe it right to the next Add-Member. The next listing shows this variation, which produces the same result in the same amount of time.
Listing 21.5. Creating objects using Add-Member with -Passthru
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
$obj = New-Object –TypeName PSObject
$obj | Add-Member NoteProperty ComputerName $os.CSName –pass |
Add-Member NoteProperty OSVersion $os.version –pass |
Add-Member NoteProperty OSArchitecture $os.osarchitecture –Pass |
Add-Member NoteProperty ProcArchitecture $proc.addresswidth –pass |
Add-Member NoteProperty BIOSSerial $bios.serialnumber –pass |
Add-Member NoteProperty Model $cs.model –pass |
Add-Member NoteProperty Manufacturer $cs.manufacturer
Write-Output $obj
You’ll see this approach in the wild, and, in fact, from an instruction perspective, it’s a great technique because it’s much clearer what’s happening. This technique doesn’t use any syntactic shortcuts, so it’s a bit easier to follow each step of the process.
21.2.4. Technique 4: using a Type declaration
This one is a variation of our Technique 1, and it’s only valid in PowerShell v3 or v4. You’re going to start with the same hash table of properties. This technique provides a more compact means of creating the new object and assigning the properties, as shown in the following listing.
Listing 21.6. Creating objects using a Type declaration
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
$obj = [pscustomobject]@{OSVersion=$os.version
Model=$cs.model
Manufacturer=$cs.manufacturer
BIOSSerial=$bios.serialnumber
ComputerName=$os.CSName
OSArchitecture=$os.osarchitecture
ProcArchitecture=$proc.addresswidth
}
Write-Output $obj
You could’ve continued to put the hash table into the $props variable and used that to create the new object, but there’s a neat trick about this technique in that it preserves the insertion order of the properties just as if you’d used [ordered].
Note
The type that we’re using is PSCustomObject. This is a placeholder for the PSObject type we used in Technique 1. You have to use PSCustomObject because in .NET terms you’re using the PSObject constructor with no parameters. Don’t try to shorten the code by substitutingPSObject for PSCustom-Object—you won’t get the results you expect.
You may have noticed with all of the previous techniques that the properties came out listed in a different order than the order you used to add them. In Technique 1, for example, you didn’t add ComputerName first, but it wound up being listed first for some reason. In many instances, you won’t care—PowerShell can work with properties in any order. Technique 4, however, preserves that order for times when you do care.
21.2.5. Technique 5: creating a new class
There’s one more technique you can use. It isn’t used much but it provides some advantages if the object will be placed on the pipeline for further processing. It can be classified as an advanced technique—not all IT pros will want to delve this far into .NET, but it’s available as an option (see the following listing).
Listing 21.7. Creating objects using a new class
$source=@"
public class MyObject
{
public string ComputerName {get; set;}
public string Model {get; set;}
public string Manufacturer {get; set;}
public string BIOSSerial {get; set;}
public string OSArchitecture {get; set;}
public string OSVersion {get; set;}
public string ProcArchitecture {get; set;}
}
"@
Add-Type -TypeDefinition $source -Language CSharpversion3
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1
$props = @{OSVersion=$os.version
Model=$cs.model
Manufacturer=$cs.manufacturer
BIOSSerial=$bios.serialnumber
ComputerName=$os.CSName
OSArchitecture=$os.osarchitecture
ProcArchitecture=$proc.addresswidth}
$obj = New-Object –TypeName MyObject –Property $props
Write-Output $obj
The script in listing 21.7 starts by creating a PowerShell here-string that holds the C# code to define the class. The class has a name, MyObject, and makes a number of statements defining the properties. In this example, the properties are all strings, but a mixture of types is allowed. And even though we don’t expect to set any values on the object, the class definition requires both GET and SET accessors; otherwise, PowerShell will throw an exception.
Add-Type is used to compile the class, which can then be used in place of PSObject when you create an object with New-Object. The object’s properties can be supplied using the technique shown here or in listing 21.6:
$obj = [MyObject]@{OSVersion=$os.version
Model=$cs.model
Manufacturer=$cs.manufacturer
BIOSSerial=$bios.serialnumber
ComputerName=$os.CSName
OSArchitecture=$os.osarchitecture
ProcArchitecture=$proc.addresswidth}
It’s worth testing $obj with Get-Member:
PS C:\> $obj | get-member
TypeName: MyObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
BIOSSerial Property string BIOSSerial {get;set;}
ComputerName Property string ComputerName {get;set;}
Manufacturer Property string Manufacturer {get;set;}
Model Property string Model {get;set;}
OSArchitecture Property string OSArchitecture {get;set;}
OSVersion Property string OSVersion {get;set;}
ProcArchitecture Property string ProcArchitecture {get;set;}
This technique is a little more complicated, but its advantage is that the individual properties on the object are typed. If you define a property as an integer and try to put a string into it, an error will be thrown.
21.2.6. What’s the difference?
Other than readability, the amount of typing required, and the preservation of the properties’ order, these techniques are all essentially the same. A few subtle differences do exist. Technique 1, our hash table approach, is generally the fastest, especially when you’re working with multiple output objects. Technique 2 is a bit slower, and Technique 3 can be significantly slower. There’s a good write-up at http://learn-powershell.net/2010/09/19/custom-powershell-objects-and-performance/ from PowerShell MVP Boe Prox, which compares the speeds of these three techniques across 10 objects, and the Add-Member technique (our Technique 3) is something like 10 times slower. So it’s worth choosing a quicker technique, and our Technique 1, which we also feel is concise and readable, was the speed winner. We haven’t tested the speed of Technique 4, which was new in PowerShell v3, to the same extent that Boe did with the other techniques, but our tests indicate that Technique 4 is about 10% faster than Technique 1. Technique 5 should be reserved for the occasions when data typing of the properties is essential.
21.3. Complex objects: collections as properties
Earlier in this chapter, in the discussion of listing 21.1, we pointed out the use of Select-Object –first 1 to ensure you only get one processor back from the WMI query. What about instances where you might get multiple objects, and where you explicitly need to keep each one of them, because they’re each different? Getting user accounts is a good example of that. You can certainly create a custom object that has multiple child objects. Essentially, you first construct a variable that contains each of those objects and then append it to a second, top-level object that will be your final output. This is a lot easier to see than to talk about, so let’s go right to the next listing.
Listing 21.8. Working with multiple objects
The script begins by retrieving information into variables, just as you’ve done in prior examples. This time, you’re getting disks and users, which may well have multiple objects on any given machine. You’re not limiting this to the first one of each; you’re grabbing whatever’s present on the machine. The fun starts when you create an empty array to hold all your custom disk objects . You then enumerate through the disks, one at a time . Each time through, you set up a hash table for the properties you want to display for a disk , and then you create the disk object using those properties . At the end of the iteration, you append that disk object to your originally empty array . Once you’ve made it through all the disks, you repeat the same basic process for the users .
Once that’s all done, you create your final output object. It includes information from the operating system but also the collections of user and disk objects you just created . That final object is output to the pipeline . The result looks something like this:
Users : {@{UserSID=S-1-5-21-29812541-3325070801-1520984716-500
; UserName=Administrator}, @{UserSID=S-1-5-21-29812541
-3325070801-1520984716-501; UserName=Guest}, @{UserSID
=S-1-5-21-29812541-3325070801-1520984716-502; UserName
=krbtgt}, @{UserSID=S-1-5-21-29812541-3325070801-15209
84716-1103; UserName=rhondah}}
OSVersion : 6.1.7601
Disks : {@{Space=42842714112; Drive=C:; FreeSpace=32443473920}
}
ComputerName : WIN-KNBA0R0TM23
SPVersion : 1
What you’re seeing in the Disks and Users properties is PowerShell’s way of displaying properties that have multiple subobjects as their contents. Each of your disks and users is being displayed as a hash table of property=value entries. If there are a number of accounts on the system, you may see “...” in the users display. This is because PowerShell will only show the first four values in a collection by default. You can change this by modifying the value contained in the $FormatEnumerationLimit variable.
You can use Select-Object to extract just one of those properties’ children in a more sensible fashion:
PS C:\> .\multitest.ps1 | select -expand users
UserSID UserName
------- --------
S-1-5-21-29812541-3325070801-15... Administrator
S-1-5-21-29812541-3325070801-15... Guest
S-1-5-21-29812541-3325070801-15... krbtgt
S-1-5-21-29812541-3325070801-15... rhondah
So this is how you can create, and ultimately access, a complex hierarchy of data by using a single output object type. This is also—finally—a good time to show you a potential use for the Format-Custom cmdlet. Check this out:
PS C:\> .\multitest.ps1 | format-custom
class PSCustomObject
{
Users =
[
class PSCustomObject
{
UserSID = S-1-5-21-29812541-3325070801-1520984716-500
UserName = Administrator
}
class PSCustomObject
{
UserSID = S-1-5-21-29812541-3325070801-1520984716-501
UserName = Guest
}
class PSCustomObject
{
UserSID = S-1-5-21-29812541-3325070801-1520984716-502
UserName = krbtgt
}
class PSCustomObject
{
UserSID = S-1-5-21-29812541-3325070801-1520984716-1103
UserName = rhondah
}
]
OSVersion = 6.1.7601
Disks =
[
class PSCustomObject
{
Space = 42842714112
Drive = C:
FreeSpace = 32442359808
}
]
ComputerName = WIN-KNBA0R0TM23
SPVersion = 1
}
Given a bunch of objects, Format-Custom will attempt to display their properties. When it runs across a property that itself contains subobjects, it’ll attempt to break those down. Parameters let you specify how deeply it’ll attempt to do this within a nested hierarchy of objects. Format-Custom is covered in more detail in chapter 9.
21.4. Applying a type name to custom objects
The custom objects you’ve created so far are all of a generic type. You can test that by piping any of them to Get-Member:
PS C:\> .\multitest.ps1 | get-member
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
ComputerName NoteProperty System.String ComputerName=WIN-KNBA0R0TM23
Disks NoteProperty System.Object[] Disks=System.Object[]
OSVersion NoteProperty System.String OSVersion=6.1.7601
SPVersion NoteProperty System.UInt16 SPVersion=1
Users NoteProperty System.Object[] Users=System.Object[]
There’s nothing wrong with all of these objects having the same type, unless you want to apply a custom format view or a custom type extension (something we cover in upcoming chapters). Those custom extensions require an object to have a unique name so that PowerShell can identify the object and apply the extension appropriately. Giving one of your objects a custom name is easy—just do so before outputting it to the command line. We’ll revise listing 21.6, as shown in listing 21.9, to add a custom type name. This technique isn’t needed if the syntax from listing 21.7 is used because you define a type name when creating the class.
Listing 21.9. Adding a type name to custom objects
The script in listing 21.9 produces the following output when piped to Get-Member:
PS C:\> .\multitest.ps1 | get-member
TypeName: My.Awesome.Type
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
ComputerName NoteProperty System.String ComputerName=WIN-KNBA0R0TM23
Disks NoteProperty System.Object[] Disks=System.Object[]
OSVersion NoteProperty System.String OSVersion=6.1.7601
SPVersion NoteProperty System.UInt16 SPVersion=1
Users NoteProperty System.Object[] Users=System.Object[]
As you can see, the custom type name was applied and is reflected in the output. Your only real concern with custom type names is that they not overlap with any other type names that might be running around inside the shell. The easiest way to ensure that uniqueness is to use a standard naming convention, within your organization, for custom type names. For example, a type name like Contoso.PowerShell.UserInfo is unique, describes the kind of information that the object holds, and is unlikely to interfere with anyone else’s efforts. We’ll show you how to put that custom type name to use in chapters 26 and 27.
21.5. So, why bother?
This may seem like an awful lot of trouble. Let’s skip back to our first complete example, in listing 21.2, and redo it in the way that a lot of PowerShell newcomers would do. The following listing shows this approach, which we consider substandard, and we’ll explain why in a moment.
Listing 21.10. Multiple objects
Get-WmiObject –Class Win32_OperatingSystem –comp localhost |
Select CSName,Version,OSArchitecture
Get-WmiObject –Class Win32_ComputerSystem –comp localhost |
Select Model,Manufacturer
Get-WmiObject –Class Win32_BIOS –comp localhost |
Select SerialNumber
Get-WmiObject –Class Win32_Processor –comp localhost |
Select –First 1 -property AddressWidth
Here’s what this script gets you:
PS C:\> .\NoObjects.ps1
CSName Version OSArchitecture
------ ------- --------------
WIN-KNBA0R0TM23 6.1.7601 64-bit
Um, wait—where’s all of the other information? The problem is that this script violates a primary PowerShell law because it’s outputting multiple kinds of objects. There’s an operating system object first, then a computer system object, then a BIOS object, then a processor object. We explained in the previous chapter that PowerShell doesn’t deal well with that situation. PowerShell sees the first object and tries to format it and then gets lost because all this other stuff comes down the pipe. So, this is a bad approach. Most folks’ second attempt will look like the next listing.
Listing 21.11. Outputting text
$os = Get-WmiObject –Class Win32_OperatingSystem –comp localhost |
Select CSName,Version,OSArchitecture
$cs = Get-WmiObject –Class Win32_ComputerSystem –comp localhost |
Select Model,Manufacturer
$bios = Get-WmiObject –Class Win32_BIOS –comp localhost |
Select SerialNumber
$proc = Get-WmiObject –Class Win32_Processor –comp localhost |
Select –first 1 -property AddressWidth
Write-Host " Name: $($os.CSName)"
Write-Host " OS Version: $($os.version)"
Write-Host " Model: $($cs.model)"
Write-Host " OS Architecture: $($os.osarchitecture)"
Write-Host " Manufacturer: $($cs.manufacturer)"
Write-Host "Proc Architecture: $($proc.addresswidth)"
Write-Host " BIOS Serial: $($bios.serialnumber)"
Here’s what you get when you run listing 21.11:
PS C:\> .\OutputText.ps1
Name: RSSURFACEPRO2
OS Version: 6.3.9600
Model: Surface Pro 2
OS Architecture: 64-bit
Manufacturer: Microsoft Corporation
Proc Architecture: 64
BIOS Serial: 036685734653
Look at all of the care that went into formatting that! Everything all lined up and pretty. Too bad it’s a waste of time. Try to reuse that information in any way whatsoever, and it’ll fail. None of the following will do anything useful at all:
· .\OutputText.ps1 | ConvertTo-HTML | Out-File inventory.html
· .\OutputText.ps1 | Export-CSV inventory.csv
· .\OutputTest.ps1 | Export-CliXML inventory.xml
This is the problem with a script that simply outputs text. And whether you output formatted text via Write-Host or Write-Output doesn’t matter; it’s still just text. PowerShell wants the structured data offered by an object, and that’s why the techniques in this chapter are so important.
If we haven’t stressed this enough, we’ll leave you with one more code example where you can create your own object out of just about anything.
Listing 21.12. Creating your own custom object
$computername=$env:computername
$prop=[ordered]@{Computername=$Computername}
$os=Get-WmiObject Win32_OperatingSystem -Property Caption,LastBootUpTime `
-ComputerName $computername
$boot=$os.ConvertToDateTime($os.LastBootuptime)
$prop.Add("OS",$os.Caption)
$prop.Add("Boot",$boot)
$prop.Add("Uptime",(Get-Date)-$boot)
$running=Get-Service -ComputerName $computername |
Where status -eq "Running"
$prop.Add("RunningServices",$Running)
$cdrive=Get-WMIObject win32_logicaldisk -filter "DeviceID='c:'" `
-computername $computername
$prop.Add("C_SizeGB",($cdrive.Size/1GB -as [int]))
$prop.Add("C_FreeGB",($cdrive.FreeSpace/1GB))
$obj=New-Object -TypeName PSObject -Property $prop
$obj.PSObject.TypeNames.Insert(0,"MyInventory")
Write-Output $obj
In this example you’re getting information from a variety of sources and building a custom object. Here’s the sample output:
Computername : RSSURFACEPRO2
OS : Microsoft Windows 8.1 Pro
Boot : 27/01/2014 17:27:00
Uptime : 6.22:30:22.8385355
RunningServices : {AdobeARMservice, Appinfo, AppMgmt, AudioEndpointBuilder...}
C_SizeGB : 232
C_FreeGB : 162.906734466553
Here, the RunningServices property is a collection of service objects. You didn’t need to use the ForEach technique as you did in listing 21.8. The $Running variable simply becomes the value of the custom property.
Listing 21.12 is the type of code you’ll want to turn into a function where you can pass a collection of computer names. The output is an object, written to the pipeline, which you can see on the screen, convert to HTML, export to a CSV file, or do just about anything else you can think of to. The bottom line is, think objects in the pipeline.
21.6. Summary
Creating output is possibly the most important thing many scripts and functions will do, and creating that output so that it can work with PowerShell’s other functionality is crucial to making a well-behaved, flexible, consistent unit of automation. Custom objects are the key, and by making them properly you’ll be assured of an overall consistent PowerShell environment.