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.

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.