Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
June 13, 2025

Creating a Markdown Tooling System Part 2

Let's continue examining my process for building a Markdown tooling system. Up to now, I've been running my code in the console. Technically, selecting the code in VS Studio and using F8 to run it. Now that I have a working prototype, the next step is to create a function that I can call from the console. I recommend you follow this process. Start with code that you run in the console, then create a function. If you are writing related functions, the last step would be to wrap them in a module. I don't think I will need to go that far for this project.

Creating a Function

Here's the function I have so far, written for PowerShell 7 so that I can take advantage of the ErrorMessage feature. My function also defines an alias, gbm, so I can call it quickly.

#requires -version 7.5
Function Get-ButtondownMetadata {
    [CmdletBinding()]
    [Alias('gbm')]
    [OutputType('ButtondownMetadata')]
    param (
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipeline,
            HelpMessage = 'Enter the path to the Markdown file containing the Buttondown newsletter metadata.'
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ }, ErrorMessage = 'Cannot find or verify the path {0}.')]
        [ValidatePattern('^.*\.md$', ErrorMessage = 'The path must point to a Markdown file.')]
        [string]$Path
    )

    Begin {
        #create a timer to measure the function runtime
        $timer = [System.Diagnostics.Stopwatch]::new()
        $timer.start()
        Write-Information "Starting $($MyInvocation.MyCommand)" -Tags runtime
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        $list = [System.Collections.Generic.List[string]]::new()
        #initialize a counter
        $k = 0
        $web = 'https://buttondown.com/behind-the-powershell-pipeline/archive/'
    } #begin
    Process {
        Write-Verbose "Processing $Path"
        Write-Information "Processing $Path" -Tags runtime
        $k++
        $content = Get-Content -Path $Path
        $list.AddRange([string[]]$content)
        $i = $list.FindIndex(0, { $args[0] -eq '---' }) + 1
        $j = $list.FindIndex($i, { $args[0] -match '---' }) - 1
        #only process if both markers found
        if ($i -AND $j) {
            Write-Verbose "Processing metadata from line $i to $j"
            $lines = $list[$i..$j]
            Write-Information "Detected metadata header:`n---`n$(($lines | Out-String).Trim())`n---" -Tags data
            $lines | where { $_ -match ':' } |
            ForEach-Object -Begin {
                #initialize the hashtable
                $meta = @{}
            } -Process {
                #split into two strings
                $split = $_ -split ':', 2
                $meta.Add($split[0].Trim(), $split[1].Trim())
            }
            Write-Information $meta -Tags data
            If ($meta.subject -AND $meta.email_type) {
                [PSCustomObject]@{
                    PSTypeName = 'ButtondownMetadata'
                    Title      = $meta.subject
                    Published  = $meta.publish_date -as [datetime]
                    Category   = $meta.email_type
                    Link       = $web + $meta.slug
                    Path       = $Path
                    Status     = $meta.status
                }
                $list.Clear()
            } #if meta info found
            else {
                Write-Warning "No Buttondown metadata found in $Path"
            }
        }
        else {
            Write-Warning "No Buttondown metadata found in $Path"
        }
        #clear the list
        $list.Clear()
    } #process
    End {
        $timer.stop()
        Write-Verbose "Processed $k files in $($timer.Elapsed)"
        Write-Verbose "Ending $($MyInvocation.MyCommand)"
        Write-Information "Ending $($MyInvocation.MyCommand)" -Tags runtime
    } #end
}

One thing I didn't focus on during the prototype phase was error handling. I'm always telling you to think about who will run your code. In this case, I am the only one who will run it and will most likely always run it on valid Markdown files imported from Buttondown. However, there are a few potential issues that could arise. I might pass a non-Markdown file. The markdown file might not have a metadata header. Or if it does, it may be different metadata. On the off chance I make a mistake, I don't want PowerShell to yell at me with ugly error messages. That's why you'll see parameter validation and error handling in the code.

Testing error handling
figure 1

The function includes my typical verbose messaging. I also added a few Write-Information messages. These are useful for troubleshooting. I saved them to a variable in the last example.

PS C:\> $v
Starting Get-ButtondownMetadata
Processing D:\buttondown\emails\a-changelog-for-the-better.md
Detected metadata header:
---
id: 666fc836-58cd-4100-b466-046a45c15bc1
subject: A Changelog for the Better
status: imported
email_type: premium
slug: a-changelog-for-the-better
publish_date: 2023-01-03T18:05:58.410000Z
---
System.Collections.Hashtable
Ending Get-ButtondownMetadata

The items in the variable are InformationRecord objects.

PS C:\> $v[2] | Select *

MessageData     : Detected metadata header:
                  ---
                  id: 666fc836-58cd-4100-b466-046a45c15bc1
                  subject: A Changelog for the Better
                  status: imported
                  email_type: premium
                  slug: a-changelog-for-the-better
                  publish_date: 2023-01-03T18:05:58.410000Z
                  ---
Source          : D:\OneDrive\behind\2025\buttondown-email-tooling\Get-ButtondownMetadata.ps1
TimeGenerated   : 6/7/2025 10:02:35 AM
Tags            : {data}
User            : PROSPERO\Jeff
Computer        : Prospero
ProcessId       : 4984
NativeThreadId  : 41908
ManagedThreadId : 8

Tagging is optional, but I like to use it to categorize my messages. I can filter the messages by tag later if needed.

Want to read the full issue?
GitHub Bluesky LinkedIn Mastodon https://jdhitsoluti…
Powered by Buttondown, the easiest way to start and grow your newsletter.