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 About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.