Objects of Your Own: `[PSCustomObject]`

[PSCustomObject]@{ } builds an object of your own with named properties, so a tool you write hands back results that Sort-Object, Where-Object and Export-Csv can work with, the way they work with PowerShell's own commands, instead of lines of text.

Journeyman · PowerShell · Updated

After this lesson you can

  • Build an object of your own with [PSCustomObject]@{ } and give each property a name and a value
  • Hand back objects instead of text from a function, so its results can be sorted, filtered and exported

Key takeaways

  • A line of text has one property, its Length, so Sort-Object, Where-Object and Export-Csv can't get at what's inside it.
  • [PSCustomObject]@{ Name = 'Banana Bread'; Minutes = 70 } builds an object with the properties you name, in the order you write them.
  • A number written without quotes stays a number, so it sorts and compares as a number.
  • A function hands back objects and leaves the formatting to whoever runs it: Format-Table inside the function breaks sorting and saving.

Your family's recipes live on index cards, in three notebooks and in a dozen browser tabs, so you start a recipe box in PowerShell: a few commands that keep every recipe in one place. The first version prints a neat line for each recipe, and it looks done. Then someone asks the first real question: "What can I make in 25 minutes?" The neat lines can't answer it.

This chapter is where the course's big idea pays off. A script is an application, and now yours becomes a real tool, with objects of its own, data it remembers, friendly output and even a window. It starts with the objects.

Text looks right, until you ask a question

Make a practice folder, then type this first version of the recipe box at the prompt (or paste it):

New-Item -ItemType Directory -Force -Path "$HOME\terminal-practice\recipe-box" | Out-Null
Set-Location "$HOME\terminal-practice\recipe-box"
function Get-Recipe {
    'Lemon Garlic Pasta - 30 min - Dinner'
    'Overnight Oats - 10 min - Breakfast'
    'Banana Bread - 70 min - Baking'
    'Black Bean Tacos - 25 min - Dinner'
}

Get-Recipe prints the four lines. Now sort them by how long they take, and save them for a spreadsheet:

Get-Recipe | Sort-Object Minutes
Get-Recipe | Export-Csv recipes.csv
Get-Content recipes.csv
Lemon Garlic Pasta - 30 min - Dinner
Overnight Oats - 10 min - Breakfast
Banana Bread - 70 min - Baking
Black Bean Tacos - 25 min - Dinner
"Length"
"36"
"35"
"30"
"34"

The sort did nothing, without a word of complaint, and the spreadsheet holds only numbers. A line of text is one thing to PowerShell: a string, whose only property is its Length. The minutes are in there for you to read, but no command can reach them.

A card with labeled lines: `[PSCustomObject]`

A good index card has labeled lines: Name:, Minutes:, Category:. Anyone can find the minutes without reading the whole card. PowerShell's version of that card is a custom object, an object you design yourself, and you build one from a hashtable:

[PSCustomObject]@{ Name = 'Lemon Garlic Pasta'; Minutes = 30; Category = 'Dinner' }
Name               Minutes Category
----               ------- --------
Lemon Garlic Pasta      30 Dinner

Read it from the right. @{ } is a lookup of names and values, from Lists and Lookups: Arrays and Hashtables: = joins each name to its value and ; separates the pairs. [PSCustomObject] in front (read "as a custom object") turns that lookup into one object, with a property for each name. It keeps the properties in the order you wrote them, so the columns come out Name, Minutes, Category. A plain hashtable shows a Name and Value row for each pair instead, in an order of its own choosing.

30 has no quotes, so it stays a number. '30' would be text, and text sorts by its first character, so '100' would come before '30'.

Your tool's results work like PowerShell's own

Now give Get-Recipe a card for each recipe. Defining it again replaces the first version:

function Get-Recipe {
    [PSCustomObject]@{ Name = 'Lemon Garlic Pasta'; Minutes = 30; Category = 'Dinner' }
    [PSCustomObject]@{ Name = 'Overnight Oats'; Minutes = 10; Category = 'Breakfast' }
    [PSCustomObject]@{ Name = 'Banana Bread'; Minutes = 70; Category = 'Baking' }
    [PSCustomObject]@{ Name = 'Black Bean Tacos'; Minutes = 25; Category = 'Dinner' }
}
Get-Recipe | Where-Object Minutes -le 25
Name             Minutes Category
----             ------- --------
Overnight Oats        10 Breakfast
Black Bean Tacos      25 Dinner

There's the answer to "what can I make in 25 minutes?". Every command from Module 3 works now: Sort-Object Minutes puts the oats first and the bread last, Group-Object Category counts two dinners, and Get-Member shows the cards for what they are:

Get-Recipe | Get-Member -MemberType NoteProperty
   TypeName: System.Management.Automation.PSCustomObject

Name     MemberType   Definition
----     ----------   ----------
Category NoteProperty string Category=Dinner
Minutes  NoteProperty int Minutes=30
Name     NoteProperty string Name=Lemon Garlic Pasta

A NoteProperty is a property you added yourself, and Minutes is an int, a whole number. Run Get-Recipe | Export-Csv recipes.csv again, and recipes.csv gets a header of "Name","Minutes","Category" and one line per recipe, ready for Excel.

That's the difference between a script that prints and a tool. The person who runs your command decides what happens next, the same way they do with Get-ChildItem or Get-Process.

Leave the formatting to the end

It's tempting to make the function's output look tidy with Format-Table on its last line. Don't. Format-Table turns the objects into instructions for drawing a table, and nothing after it can read them. Say the function collects the four cards in $recipes and ends with $recipes | Format-Table. The table looks the same, but Get-Recipe | Where-Object Minutes -le 25 shows all four recipes, and a saved spreadsheet is nonsense:

Get-Recipe | Export-Csv recipes.csv
Get-Content recipes.csv -TotalCount 3
"ClassId2e4f51ef21dd47e99d3c952918aff9cd","pageHeaderEntry","pageFooterEntry","autosizeInfo","shapeInfo","groupingEntry"
"033ecb2bc07a4d43b5ef94ed5a35d280",,,,"Microsoft.PowerShell.Commands.Internal.Format.TableHeaderInfo",
"9e210fe47d09416682b841769c78b8a3",,,,,

The same goes for Write-Host and for building a sentence around each recipe. A function's job ends at the object; Format-Table, Sort-Object and Export-Csv belong to whoever runs it. [PSCustomObject] works the same in Windows PowerShell 5.1 and on a Mac or Linux.

Two ways to hand back a recipe. On the left, a strip of paper reading Lemon Garlic Pasta - 30 min - Dinner, and Sort-Object, Where-Object and Export-Csv each shrug at it. On the right, an index card with three labeled lines, Name: Lemon Garlic Pasta, Minutes: 30, Category: Dinner, and the same three commands each reading the line they need

Check yourself

2 questions to try on the lesson page.

So what

Objects in, objects out: give your tool's results names and values with [PSCustomObject], and everything PowerShell can do with its own commands' output, it can now do with yours.

Key terms

Cheat sheets

Related