An object-oriented shell like PowerShell was long thought impossible because objects typically contain a wealth of information, and Administrators would be overwhelmed with information if all object properties would always be outputted to the shell.
Overview
I started this article because in a recent tip I created a function called Get-Software
which returns tons of information about installed programs - in fact so much information that it simply felt overwhelming to most users and the common daily scenarios.
That’s why I added a way for the function to return only a few default properties while the user was still able to use any hidden property when needed. This technique is pretty slick but not widely known, and it is not the only one available. So I decided to dive a bit into the PowerShell type system and look at it from a users perspective, then from an authors perspective.
This article is split in two parts:
- In the first part, I’ll explain what the PowerShell type system is and how it helps you every day. That’s valuable for beginners or auto-didacts that want to complete their knowledge. Seasoned PowerShell veterans can skip this part.
- In the second part, you switch perspective: rather than consuming the PowerShell type system like a end user does, you act as an author and begin creating your own PowerShell functions and modules that build on top of the PowerShell type system and actively embrace it. This way, your own PowerShell functions may become a lot more usable for your end users.
Voilá: The PowerShell Type System
PowerShell is one of the first fully object-oriented shells around, and there is a reason why most other shells are not: in order for an object-oriented shell to be manageable, you need an object visualizer. One of the key jobs for this visualizer is to protect the user from information overflow.
Whenever you output objects to the PowerShell console, they get converted to text, and the internal type system makes sure you see only a subset of all available properties. By limiting the output to only a few properties, Admins can continue to focus on what’s important.
Have a look: this command returns a file:
Get-Item -Path c:\windows\explorer.exe
Thanks to the internal PowerShell type system, the output is restricted to only the typically used properties which look familiar to Admins:
Directory: C:\windows
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 01.03.2020 10:38 4622280 explorer.exe
Looking At Pure Objects
In reality, Get-Item
always returns rich .NET objects. As long as you pipe the results to another command or save the results to a variable, the .NET object(s) stay intact. The stripping occurs only when you output the information to the console
By appending Select-Object
before the objects are outputted, you can decide which properties you want to see. Use “*” to see all properties:
Get-Item -Path c:\windows\explorer.exe | Select-Object -Property *
Actually,
Select-Object
creates an object clone and essentially strips off all type information.# original object has distinct type information attached to it: $file = Get-Item -Path c:\windows\explorer.exe $file.PSTypeNames
System.IO.FileInfo System.IO.FileSystemInfo System.MarshalByRefObject System.Object
So it is not
Select-Object
that “overrules” the type system. Rather, any custom object with missing type information shows all of its properties:# Cloned objects returned by Select-Object change the type of the object, # and since these generic types do not exist in the hint list # of the type system, all properties are shown: $file = Get-Item -Path c:\windows\explorer.exe | Select-Object -Property * $file.PSTypeNames
Selected.System.IO.FileInfo System.Management.Automation.PSCustomObject System.Object
Get-Item
returns the very same information. Now, though, way more properties are shown, and because they wouldn’t fit horizontally, PowerShell automatically switches from table view to list view.
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\windows\explorer.exe
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName : explorer.exe
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
Mode : -a----
VersionInfo : File: C:\windows\explorer.exe
InternalName: explorer
OriginalFilename: EXPLORER.EXE.MUI
FileVersion: 10.0.18362.650 (WinBuild.160101.0800)
FileDescription: Windows Explorer
Product: Microsoft® Windows® Operating System
ProductVersion: 10.0.18362.650
Debug: False
Patched: False
PreRelease: False
PrivateBuild: False
SpecialBuild: False
Language: English (United States)
BaseName : explorer
Target : {C:\Windows\WinSxS\amd64_microsoft-windows-explorer_31bf3856ad364e35_10.0.18362.693_none_a7549da20b808a40\explorer.exe}
LinkType : HardLink
Name : explorer.exe
Length : 4622280
DirectoryName : C:\windows
Directory : C:\windows
IsReadOnly : False
Exists : True
FullName : C:\windows\explorer.exe
Extension : .exe
CreationTime : 01.03.2020 10:38:51
CreationTimeUtc : 01.03.2020 09:38:51
LastAccessTime : 01.03.2020 10:38:51
LastAccessTimeUtc : 01.03.2020 09:38:51
LastWriteTime : 01.03.2020 10:38:51
LastWriteTimeUtc : 01.03.2020 09:38:51
Attributes : Archive
Working With Objects
Because of this clever architecture, PowerShell commands provide a quick overview but you always keep the option to see more. This is important to understand.
In user groups, you often come across questions like “command xyz won’t give me the information abc, where can I get it?”. Most likely, the command returned it already, but it was hidden. So the best answer would be: “append
| Select-Object *
and check again!”
Let’s take a quick look at the typical PowerShell workflow:
1. Run Command
First, run a command and look at the default output, for example:
Get-Process -Id $pid
Take a look at the results to see if the information surfaces that you need:
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
1235 93 580128 647576 53,30 21500 1 powershell_ise
2. Look Again With All Properties Visible
If the command won’t give you the information you need, run it through | Select-Object * -First 1
. This gives you all properties for the first (random) returned object.
Without -First 1
you’d see a long list of repeating information if the command returned many objects. To investigate properties, it’s better to focus on one object.
Get-Process -Id $pid | Select-Object -Property * -First 1
Look at the full range of properties, and take a note of any that are useful to you:
Name : powershell_ise
Id : 21500
PriorityClass : Normal
FileVersion : 10.0.18362.1 (WinBuild.160101.0800)
HandleCount : 1255
WorkingSet : 651796480
PagedMemorySize : 590516224
PrivateMemorySize : 590516224
VirtualMemorySize : 1844117504
TotalProcessorTime : 00:00:54.0937500
SI : 1
Handles : 1255
VM : 6139084800
WS : 651796480
PM : 590516224
NPM : 94384
Path : C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell_ISE.exe
Company : Microsoft Corporation
CPU : 54,109375
ProductVersion : 10.0.18362.1
Description : Windows PowerShell ISE
Product : Microsoft® Windows® Operating System
__NounName : Process
BasePriority : 8
ExitCode :
HasExited : False
ExitTime :
Handle : 4004
SafeHandle : Microsoft.Win32.SafeHandles.SafeProcessHandle
MachineName : .
MainWindowHandle : 8192226
MainWindowTitle : C:\Users\tobia
MainModule : System.Diagnostics.ProcessModule (PowerShell_ISE.exe)
MaxWorkingSet : 1413120
MinWorkingSet : 204800
Modules : {System.Diagnostics.ProcessModule (PowerShell_ISE.exe), System.Diagnostics.ProcessModule (ntdll.dll), System.Diagnostics.ProcessModule (MSCOREE.DLL),
System.Diagnostics.ProcessModule (KERNEL32.dll)...}
NonpagedSystemMemorySize : 94384
NonpagedSystemMemorySize64 : 94384
PagedMemorySize64 : 590516224
PagedSystemMemorySize : 1645200
PagedSystemMemorySize64 : 1645200
PeakPagedMemorySize : 721035264
PeakPagedMemorySize64 : 721035264
PeakWorkingSet : 806756352
PeakWorkingSet64 : 806756352
PeakVirtualMemorySize : 1941008384
PeakVirtualMemorySize64 : 6235975680
PriorityBoostEnabled : True
PrivateMemorySize64 : 590516224
PrivilegedProcessorTime : 00:00:11.7968750
ProcessName : powershell_ise
ProcessorAffinity : 255
Responding : True
SessionId : 1
StartInfo : System.Diagnostics.ProcessStartInfo
StartTime : 28.03.2020 23:08:17
SynchronizingObject :
Threads : {24448, 20968, 4456, 18388...}
UserProcessorTime : 00:00:42.3281250
VirtualMemorySize64 : 6139084800
EnableRaisingEvents : False
StandardInput :
StandardOutput :
StandardError :
WorkingSet64 : 651796480
Site :
Container :
3. Fine-Tune Select-Object
Replace “*” with the comma-separated list of properties you find useful:
Get-Process -Id $pid | Select-Object -Property Name, Description, MainWindowTitle, StartTime
When you compare the result with the default result you got in step 1, you immediately understand how flexible PowerShell commands are: the very same Get-Process
command now provides you with a completely different output:
Name Description MainWindowTitle StartTime
---- ----------- --------------- ---------
powershell_ise Windows PowerShell ISE C:\Users\tobia 28.03.2020 23:08:17
4. Look At Parameters That Unlock Information
If you still can’t see the properties you need, as a last resort look at the cmdlet parameters to see if there is any that can add more information.
For example, if you wanted to find out the owner of a process, it turns out that Get-Process
adds this information only on request (because it is a rather expensive operation, plus it requires Admin privileges):
Get-Process -Id $pid -IncludeUserName
Provided you ran the command with elevated privileges, you now see a property that wasn’t there before: UserName.
Handles WS(K) CPU(s) Id UserName ProcessName
------- ----- ------ -- -------- -----------
848 297024 6,14 4644 DESKTOP-8DVNI43\tobia powershell_ise
Whenever it is especially expensive to retrieve or calculate information, it is likely that there are distinct properties that need to be set.
For example, if you want to see all properties of Active Directory users returned by Get-AdUser
, you need to add the parameter -Properties *
to the command. Else, you get only a few basic properties.
Behind The Scenes: Out-Default
All of this occurs when objects are outputted to the PowerShell console. Let’s take a quick look at the architecture of this part.
PowerShell checks whether your command is a variable assignment, and if it is not, it secretly appends Out-Default
to your command. Out-Default
is the magic command that takes the object, strips properties, and returns a rich text representation.
Here are a couple of fun proof-of-concepts that illustrate how this all works:
Silencing Output
When you overwrite Out-Default
with a function that does not return anything, you can silence PowerShell, and no output is written to the console anymore:
# silence output
function Out-Default {}
# no output:
Get-Service
Likewise, when you output anything from your version of Out-Default
, this is what gets outputted instead of the original output whenever you run a command:
# overwrite output
filter Out-Default { "censored" }
# returns "censored" per each service (run lines individually!)
Get-Service
A filter executes the code for each incoming pipeline element. If you replace filter by function, you get only one “censored” for all services.
Bypassing Formatting System
When your version of Out-Default
echoes back the incoming objects, you are bypassing the formatting system:
filter Out-Default { $_ }
You now witness how PowerShell would behave if objects were converted to text using only their built-in method ToString():
Get-Service
The result is a plain text list of service names:
AarSvc_e5d0a
AdobeARMservice
AJRouter
ALG
AppIDSvc
Appinfo
AppMgmt
...
Bypassing Type System
Finally, let’s replace Out-Default
with a more sophisticated proxy function that strips all type information from objects before they are processed:
function Out-Default
{
param
(
[switch]
$Transcript,
[Parameter(ValueFromPipeline)]
[psobject]
$InputObject
)
begin
{
# prepend a foreach-object, and strip off all type information from incoming objects before
# calling the "real" Out-Default with its parameters:
$scriptCmd = {& Foreach-Object { $_.PSTypeNames.Clear(); $_ } | Microsoft.PowerShell.Core\Out-Default @PSBoundParameters }
# use a steppable pipeline to use "our" pipeline in place of the original command:
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
# do default actions for any incoming object:
process { $steppablePipeline.Process($_) }
# do default action when pipeline is done:
end { $steppablePipeline.End() }
}
When you run this code, Out-Default
now is internally replaced by this command:
| Foreach-Object { $_.PSTypeNames.Clear(); $_ } | Out-Default
Before objects get to the original Out-Default
, the type information in the hidden property PSTypeNames is cleared. Without the type information, the PowerShell type system is “blinded” and no longer finds the types in his hinting tables. So now you always get to see all properties.
Just imagine PowerShell would always look and work like this - scary thought:
dir c:\windows
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\windows\addins
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName : addins
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : True
Name : addins
FullName : C:\windows\addins
Parent : windows
Exists : True
Root : C:\
Extension :
CreationTime : 19.03.2019 05:52:44
CreationTimeUtc : 19.03.2019 04:52:44
LastAccessTime : 26.08.2019 18:59:41
LastAccessTimeUtc : 26.08.2019 16:59:41
LastWriteTime : 19.03.2019 05:52:52
LastWriteTimeUtc : 19.03.2019 04:52:52
Attributes : Directory
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\windows\ADFS
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName : ADFS
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : True
Name : ADFS
FullName : C:\windows\ADFS
Parent : windows
Exists : True
Root : C:\
Extension :
CreationTime : 12.11.2019 16:14:51
CreationTimeUtc : 12.11.2019 15:14:51
LastAccessTime : 12.11.2019 16:14:51
LastAccessTimeUtc : 12.11.2019 15:14:51
LastWriteTime : 12.11.2019 16:14:51
LastWriteTimeUtc : 12.11.2019 15:14:51
Attributes : Directory
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\windows\appcompat
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSChildName : appcompat
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : True
Name : appcompat
FullName : C:\windows\appcompat
Parent : windows
Exists : True
Root : C:\
Extension :
CreationTime : 19.03.2019 05:52:44
CreationTimeUtc : 19.03.2019 04:52:44
LastAccessTime : 08.09.2019 12:58:54
LastAccessTimeUtc : 08.09.2019 10:58:54
LastWriteTime : 08.09.2019 12:58:42
LastWriteTimeUtc : 08.09.2019 10:58:42
Attributes : Directory
...
At the same time, you get a good clue of how the type system works: any object has a hidden property PSTypeNames that lists the types that describe this object. PowerShell traverses this list and checks whether one of these types is found in its internal hint lists.
There are actually potentially two hint lists for a type: formats and type information. Combined, they define which default properties should be visible, in which format these should appear (i.e. table, or list), and what the default column width and alignment for any property should be.
So PowerShell can fully customize the way how objects are converted to text, and so can you in just a moment.
If so, PowerShell uses the type and formatting hints to convert the object to text.
If the type is not found, all properties are returned, and PowerShell uses a rule-of-thumb universal formatting rule:
- Objects with less than five properties are formatted as a table.
- Objects with five or more properties are formatted as a list.
Restoring Out-Default
To remove all customizations and return to the built-in Out-Default
behavior, delete your function:
Remove-Item -Path function:Out-Default
PowerShell now uses the built-in cmdlet Out-Default
again.
PowerShell functions have a higher precedence than cmdlets. In the examples above, I temporarily overruled the built-in cmdlet ` Out-Default` by defining a function with the same name.
Using Hints For PowerShell Functions
Most PowerShell functions do not take notice of the features I just described. That’s perfectly ok provided your functions return default .NET objects: the PowerShell type system knows how to properly handle these object types regardless of who produced them.
Custom Objects Require Your Love
Once your functions return custom objects, things become messy, though. Custom objects are typically created in one of these ways:
- Select-Object: When you use
Select-Object
, the result is always a custom object - [PSCustomObject]: When you “convert” a hashtable to [PSCustomObject], you also create custom objects
When you append properties via
Add-Member
, you are not creating custom objects.Add-Member
adds members to a type without changing the type. The PowerShell type system treats such objects as if they were of the underlying object type that you started with.
Since custom objects are always unique and never known to PowerShell, the type system can’t help preventing information overflow, and the user always sees all information surfacing in the console.
When this happens, PowerShell uses its default formatting rule: with four or less properties, your objects are formatted as a table, else as a list. This is why many PowerShell scripters try and limit their custom objects to four or less properties to keep the output formatted as table. There are much better ways for this, though, and without the need to restrict information to only four properties.
Let’s take a look at how authors of PowerShell functions can tell the type system the names of default properties so that the type system can work with your custom objects, too.
Adding PSStandardMembers
Whenever PowerShell discovers the secret property PSStandardMembers on an object, it expects to find a PSPropertySet with the names of the default properties.
By adding this property to objects that your function emits, you control the default properties of your returned objects:
function Test-Output
{
# create a custom object
$person = [PSCustomObject]@{
LastName = 'Weltner'
FirstName = 'Tobias'
Gender = 'm'
Conference = 'psconf.eu'
Country = 'Germany'
Twitter = 'tobiaspsp'
}
# create a PSPropertySet with the default property names
[string[]]$visible = 'LastName','Country','Twitter'
[Management.Automation.PSMemberInfo[]]$visibleProperties = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet',$visible)
# before outputting, append the PSStandardMembers property:
$person |
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $visibleProperties -PassThru
}
When you run Test-Output
, only the properties LastName, Country, and Twitter are returned, and since these are less than five, they are formatted as table:
Test-Output
LastName Country Twitter
-------- ------- -------
Weltner Germany tobiaspsp
In reality, Test-Output
returned an object that contains more information. Use Select-Object
to choose which properties to display:
Test-Output | Select-Object -Property *
LastName : Weltner
FirstName : Tobias
Gender : m
Conference : psconf.eu
Country : Germany
Twitter : tobiaspsp
This approach is best when you ship stand-alone functions because everything required happens inside of your function, and there are no prerequisites to consider.
However, with this approach you are cheating a little bit because you are submitting the list of default properties per object and not per type. Theoretically, you could choose different default properties per object, even if they were of same type - no good for consistency. In addition, if you ever chose to pick different default properties, you’d have to touch all of your code that added the information to objects. You can’t centrally manage this approach.
Adding PSTypeNames And Updating PowerShell
PowerShell typically handles formatting per type, so the best approach is to add information about your type to the internal PowerShell type system once and leave the rest to PowerShell.
Any customization of the PowerShell type system is per session, so once you close PowerShell, all customizations are lost and need to be re-imported the next time you launch PowerShell. More about this in a moment.
Which raises the question: what is the type of custom objects you create? And the answer is: whatever you want! Assign any custom type! You can make up your own. Just make sure it is unique.
1. Add Your Own Custom Type
All objects have a secret property called PSCustomTypes which is a list. Use its method Add() to add new types. You can name them any way you want (as long as they don’t collide with existing types):
function Test-Output
{
# create a custom object
$person = [PSCustomObject]@{
LastName = 'Weltner'
FirstName = 'Tobias'
Gender = 'm'
Conference = 'psconf.eu'
Country = 'Germany'
Twitter = 'tobiaspsp'
}
# assign your own custom type to your custom object:
$person.PSTypeNames.Add("psconfDelegate")
# return object
$person
}
When you run Test-Output
, you’ll notice that still all properties show. When you look at the type names, you’ll see why:
$result = Test-Output
$result.PSTypeNames
The list of types does not match any type in PowerShells internal hint lists (yet):
System.Management.Automation.PSCustomObject
System.Object
psconfDelegate
2. Add Your Custom Type To PowerShells Hint Lists
So in a second step, you need to tell PowerShell what the default types for your new type psconfDelegate are:
Update-TypeData -TypeName psconfDelegate -DefaultDisplayPropertySet 'LastName','Country','Twitter'
Once you run this line and add the information to the type system, Test-Output
returns only the listed properties by default:
Test-Output
LastName Country Twitter
-------- ------- -------
Weltner Germany tobiaspsp
Use Select-Object
to see all available properties:
Test-Output | Select-Object -Property *
LastName : Weltner
FirstName : Tobias
Gender : m
Conference : psconf.eu
Country : Germany
Twitter : tobiaspsp
The great benefit of this approach is that it is type-oriented and centrally managed. If you decide to change the default properties one day, all you need to do is update the information in PowerShells type system using a simple call to Update-TypeData
.
On the other hand, this approach requires that you do call Update-TypeData
, and if you are shipping a stand-alone function, the previous approach is much better suited.
In practice, this approach works well if you ship your functions inside of PowerShell modules: simply add Update-TypeData
to your module code so it gets executed when the module is imported. This way, the PowerShell type system is automatically updated when your functions run.
Use ps1xml Files
The most flexible and professional way to import custom type information into PowerShells type system is to use a stand-alone .ps1xml file.
With this file, you can define the default properties plus the formatting of these properties. So you can also define the width of table columns as well as their alignment.
Designing .*.ps1xml File
In our example, the file would look like this:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
<ViewDefinitions>
<View>
<Name>Default</Name>
<ViewSelectedBy>
<TypeName>psconfDelegate</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>14</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>LastName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Country</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Twitter</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
ViewSelectedBy lists the type(s) that this format applies to. TableHeaders defines the formatting of table columns, and TableRowEntries defines the properties to show by default. Obviously, the number of TableHeaders must match the number of TableRowEntries.
PowerShell uses a lot of .format.ps1xml and .types.ps1xml files to drive its type system. You can see these here (only look, do not touch, or bad things may happen):
Get-ChildItem -Path $PSHOME -Filter *.ps1xml -Recurse
Formatting information like the ones in our own xml above go into .format.ps1xml files and are imported into the type system via
Update-FormatData
. Type information such as adding properties and methods is done via .types.ps1xml files. These are imported viaUpdate-TypeData
.There is some overlap between formatting and type extensions. In the previous example I used
Update-TypeData
to define default properties for types. With .format.ps1xml files, you need to useUpdate-FormatData
instead.
Loading *.format.ps1xml File
Save above file with the file extension .format.ps1xml. You can now manually load it into PowerShells type system:
Update-FormatData -PrependPath c:\...\yourfile.format.ps1xml
By using the parameter -PrependPath you are adding the new formatting information at the top and overwrite (shadow) potentially existing formatting information.
If you use the parameter -AppendPath, you are adding your new information at the bottom of the pile instead.
Once done, run Test-Output
again. The output formatting immediately changed:
LastName Country Twitter
-------- ------- -------
Weltner Germany tobiaspsp
The column width is now defined per column, and the column Twitter is wider and uses right-alignment.
AutoLoading Via Module Manifest
Typically, .ps1xml files are used inside PowerShell modules, and it’s really simple:
- Make sure your functions add their custom types to their output objects
- Add a .ps1xml file per custom type to your module
In modules, the preferred way of loading .ps1xml files is via the module manifest. The manifest is a .psd1 file with a number of key-value pairs. The key FormatsToProcess is an array of relative paths to the .ps1xml files in your module.
Let’s assume you created a subfolder named Formats in your module folder. Then in your manifest, you’d add this key:
FormatsToProcess = @("Formats/type1.ps1xml", "Formats/anothertype.ps1xml", "Formats/onemore.ps1xml")
What’s Next
Make sure you have PowerShell Conference EU 2020 on your radar. That’s the place to be in June if you enjoy stuff like this. See you there!