Whenever I teach or present on PowerShell scripting, I'm always talking about writing objects to the pipeline. Most of the time you can simply let PowerShell format and display output of your command to the best of its ability. However, you may wish to take matters into your own hands and create custom output. For example, when your run Get-Process PowerShell displays a formatted table. But you know that the display is not necessary the underlying object that you see with Get-Member. PowerShell defines a default view for Process objects. You can do the same thing with your commands.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Creating Custom Output
Here's a relatively simple script that writes a custom object to the pipeline with properties derived from a number of WMI classes.
Function Get-SysInfo { [cmdletbinding()] Param( [Parameter(Position = 0, ValueFromPipeline)] [ValidateNotNullorEmpty()] [string]$Computername = $env:computername, [PSCredential]$Credential ) Begin { $cimParams = @{ Classname = "" CimSession = "" Property = "*" ErrorAction = "stop" } } Process { Try { $sess = New-CimSession @PSBoundParameters $cimParams.cimSession = $sess } Catch { Throw $_ } $cimParams.Classname = "Win32_Operatingsystem" $cimParams.Property = "CSName", "Caption","Version" Try { $os = Get-CimInstance @cimParams } Catch { Throw $_ } $cimParams.Classname = "Win32_Computersystem" $cimParams.Property = "Manufacturer", "Model" $cs = Get-CimInstance @cimParams $cimParams.Classname = "Win32_Process" $cimParams.Property = "Name" $procs = Get-CimInstance @cimParams $cimParams.Classname = "Win32_Service" $cimParams.Filter = "State = 'running'" $running = Get-Ciminstance @cimParams [PSCustomobject]@{ PSTypeName = "mySysInfo" Computername = $os.CSName OS = $os.Caption Version = $os.Version System = ("{0} {1}" -f $cs.Manufacturer.Trim(), $cs.Model.Trim()) Services = $Running.Count Processes = $Procs.count } $cimParams.remove("filter") Remove-CimSession $sess } #close Process End { #not used } } #close function
I'm defining a custom type name as part of the custom object.
Let's say I prefer the default output to be a table instead of a list. This will require a table definition stored in a format.ps1xml file. These files can be tricky to create. I used to find something in an existing file like $PSHome\DotnetTypes.format.ps1xml and copy it into a new file which I then updated. But now I have a much better solution.
New-PSFormatXML
My PSScriptTools module (which you can install from the PowerShell Gallery) now includes a command called New-PSFormatXML. The command is designed to analyze an object and by default create a table view of all properties, although you can specify which properties to include. The format.ps1xml file will autosize the table but you can remove the directive and use the widths which are best guesses. Expect some trial and error when defining a new view.
Creating a new file is as easy as this:
Get-SysInfo | New-PSFormatXML -Path .\mySysInfo.format.ps1xml
You only need a single instance of an object. You can pipe multiple objects to New-PSFormatXML but subsequent ones will be ignored. The command will generate an xml file like this:
<?xml version="1.0" encoding="UTF-8"?> <!-- format type data generated 02/18/2019 12:19:49 by BOVINE320\Jeff --> <Configuration> <ViewDefinitions> <View> <!--Created 02/18/2019 12:19:49 by BOVINE320\Jeff--> <Name>default</Name> <ViewSelectedBy> <TypeName>mySysInfo</TypeName> </ViewSelectedBy> <TableControl> <!--Delete the AutoSize node if you want to use the defined widths.--> <AutoSize /> <TableHeaders> <TableColumnHeader> <Label>Computername</Label> <Width>15</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>OS</Label> <Width>27</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Version</Label> <Width>13</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>System</Label> <Width>20</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Services</Label> <Width>11</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Processes</Label> <Width>12</Width> <Alignment>left</Alignment> </TableColumnHeader> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <!-- By default the entries use property names, but you can replace them with scriptblocks. <Scriptblock>$_.foo /1mb -as [int]</Scriptblock> --> <TableColumnItem> <PropertyName>Computername</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>OS</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Version</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>System</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Services</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Processes</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> </ViewDefinitions> </Configuration>
From here I could edit the file as necessary. But since I'm happy displaying all the properties in a table I'll update PowerShell with this information.
Update-FormatData -AppendPath .\mySysInfo.format.ps1xml
Now when I run my function, I get a table display by default.
Adding More Views
I can also create additional views.
Get-SysInfo | New-PSFormatXML -Path .\mySysInfo.format.ps1xml -Properties "Computername","OS","Version" -ViewName OS -Append
Again, I modify the view, even adding custom properties. Here's my revised format file.
<?xml version="1.0" encoding="UTF-8"?> <!-- format type data generated 02/18/2019 12:30:50 by BOVINE320\Jeff --> <Configuration> <ViewDefinitions> <View> <!--Created 02/18/2019 12:30:50 by BOVINE320\Jeff--> <Name>default</Name> <ViewSelectedBy> <TypeName>mySysInfo</TypeName> </ViewSelectedBy> <TableControl> <!--Delete the AutoSize node if you want to use the defined widths.--> <AutoSize /> <TableHeaders> <TableColumnHeader> <Label>Computername</Label> <Width>15</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>OS</Label> <Width>27</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Version</Label> <Width>13</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>System</Label> <Width>20</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Services</Label> <Width>11</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Processes</Label> <Width>12</Width> <Alignment>left</Alignment> </TableColumnHeader> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <!-- By default the entries use property names, but you can replace them with scriptblocks. <Scriptblock>$_.foo /1mb -as [int]</Scriptblock> --> <TableColumnItem> <PropertyName>Computername</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>OS</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Version</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>System</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Services</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Processes</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> <View> <!--Created 02/18/2019 12:31:00 by BOVINE320\Jeff--> <Name>OS</Name> <ViewSelectedBy> <TypeName>mySysInfo</TypeName> </ViewSelectedBy> <TableControl> <TableHeaders> <TableColumnHeader> <Label>Computername</Label> <Width>15</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>OS</Label> <Width>40</Width> <Alignment>left</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>Version</Label> <Width>13</Width> <Alignment>right</Alignment> </TableColumnHeader> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <!-- By default the entries use property names, but you can replace them with scriptblocks. <Scriptblock>$_.foo /1mb -as [int]</Scriptblock> --> <TableColumnItem> <PropertyName>Computername</PropertyName> </TableColumnItem> <TableColumnItem> <Scriptblock>$($_.OS -replace "^Microsoft","").trim()</Scriptblock> </TableColumnItem> <TableColumnItem> <PropertyName>Version</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> </ViewDefinitions> </Configuration>
In this view I'm using a scriptblock to define the OS property that strips off 'Microsoft' from the name. To use I update the format data and pipe the command to Format-Table specifying the view name.
Make it Pretty
Now there is no reason not have pretty or at least meaningful output from your functions. If you are writing custom objects to the pipeline, as long as you define a typename, it should now be much easier to create the formatting directives. In your module manifest, specify your format.ps1xml files in the FormatsToProcess section.
I hope you find this a useful addition to your PowerShell toolbox. I know this is something I will be using all the time now in my work.
I really love this. I’ve played around with format.ps1xml files but it’s usually a matter of finding one that’s close and cloning it. This looks like a super step forward from that.