Behind the PowerShell Pipeline

Behind the PowerShell Pipeline

Archives
Log in
Subscribe
November 10, 2026

Revisiting ConvertTo-MarkdownTable

In this issue:

  • Overview
  • Introduction
  • String It Along
  • Objectify It
    • Extending the Type
    • Formatting the Type
  • ConvertTo-MarkdownTable Revised
  • Summary

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 ' |' }
}
Want to read the full issue?
Already a paid subscriber? Click here to log in.
GitHub
Bluesky
LinkedIn
Mastodon
jdhitsolutions.github.io
Powered by Buttondown, the easiest way to start and grow your newsletter.