Revisiting ConvertTo-MarkdownTable
In this issue:
Overview
I revisit my solution to the September 2026 scripting challenge that converted PowerShell command output to a Markdown table, and explore ways to make the solution more object-focused which is the the more PowerShell way to think.
Introduction
A few weeks ago I shared my solution to the PowerShell scripting challenge for September. The goal was to create a PowerShell function that would accept pipeline input and create a Markdown table. The solution I shared met the challenge, but I wondered if I was meeting the spirit of the challenge. That is, was it a good use of PowerShell. Ultimately, it depends on how you expect someone to use your command. I assumed I would normally copy the output to the clipboard and paste it into a Markdown file. However, what about use cases that I can't imagine? How can I make my command flexible enough?
String It Along
The function that I shared last time allowed me to use it in a PowerShell pipelined expression:
Get-Service b* | Select-Object Name,Status,StartType,DisplayName | ConvertTo-MarkdownTable
This creates this string output:
| Name | Status | StartType | DisplayName |
| :--- | :--- | :--- | :--- |
| BcastDVRUserService_11a142 | Stopped | Manual | GameDVR and Broadcast User Service_11a142 |
| BDESVC | Stopped | Manual | BitLocker Drive Encryption Service |
| BFE | Running | Automatic | Base Filtering Engine |
| BITS | Running | Automatic | Background Intelligent Transfer Service |
| BluetoothUserService_11a142 | Running | Manual | Bluetooth User Support Service_11a142 |
| BrokerInfrastructure | Running | Automatic | Background Tasks Infrastructure Service |
| BTAGService | Running | Manual | Bluetooth Audio Gateway Service |
| BthAvctpSvc | Running | Manual | AVCTP service |
| bthserv | Running | Manual | Bluetooth Support Service |
That creates this table:
| Name | Status | StartType | DisplayName |
|---|---|---|---|
| BcastDVRUserService_11a142 | Stopped | Manual | GameDVR and Broadcast User Service_11a142 |
| BDESVC | Stopped | Manual | BitLocker Drive Encryption Service |
| BFE | Running | Automatic | Base Filtering Engine |
| BITS | Running | Automatic | Background Intelligent Transfer Service |
| BluetoothUserService_11a142 | Running | Manual | Bluetooth User Support Service_11a142 |
| BrokerInfrastructure | Running | Automatic | Background Tasks Infrastructure Service |
| BTAGService | Running | Manual | Bluetooth Audio Gateway Service |
| BthAvctpSvc | Running | Manual | AVCTP service |
| bthserv | Running | Manual | Bluetooth Support Service |
Technically, my function is writing 11 string objects to the pipeline.
PS C:\> Get-Service b* | Select-Object Name,Status,StartType,DisplayName | ConvertTo-MarkdownTable | Measure-Object | Select-Object Count
Count
-----
11
There's nothing inherently wrong with strings. They are still objects. But I'm not giving the user much flexibility.
Objectify It
Instead of writing strings to the pipeline, what if my function created a custom object? I can use the same code to create the contents of the Markdown table:
$p = Get-Process pwsh -IncludeUserName |
Select-Object ID, WorkingSet, Name, UserName, StartTime,
@{Name = 'RunTime'; Expression = { New-TimeSpan -Start $_.StartTime -End (Get-Date) } }
$align = @{
Runtime = 'right'
Username = 'center'
ID = 'center'
}
$p[0].PSObject.Properties | ForEach-Object -Begin {
#define an ordered hashtable to indicate the property name and its alignment
$headings = [ordered]@{}
} -Process {
$alignment = ($align.ContainsKey($_.Name)) ? $align[$_.Name] : 'left'
$headings.Add($_.Name, $alignment)
}
$alignBar = $headings.GetEnumerator() | ForEach-Object {
switch ($_.Value) {
'left' { ':---' }
'center' { ':---:' }
'right' { '---:' }
}
} | Join-String -OutputPrefix '| ' -OutputSuffix ' |' -Separator ' | '
But instead of creating strings from the output, I can create a custom object.
$mdTable = [PSCustomObject]@{
PSTypeName = 'PSMarkdownTable'
ColumnHeading = $headings.GetEnumerator() | Join-String -Property Name -Separator ' | ' -OutputPrefix '| ' -OutputSuffix ' |'
AlignmentHeading = $alignBar
TableValues = foreach ($item in $p) { $item.PSObject.Properties | Join-String -Property Value -Separator ' | ' -OutputPrefix '| ' -OutputSuffix ' |' }
}