Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Archives
Log in
Subscribe
August 7, 2026

Another Command Metadata Technique

In this issue:

  • Using MultiLine comments
  • AST Parsing
  • Using Structured Data
  • PowerShell Tooling
    • New-FunctionMetadata
    • Get-FunctionMetadata
  • Summary

Last time I shared with you a technique I used in the PSScriptTools module to embed tags into individual commands. Not the script file, but the command itself. My initial concept used a simple comment string and parse it from the command script block using PowerShell's Abstract Syntax Tree (AST). However, with a little planning I can take this idea and expand it. Perhaps there is other command metadata I want to embed and retrieve.

One thing to keep in mind is that because we're talking about data, it needs to be somewhat structured, consistent, and predictable.

Using MultiLine comments

The easiest way to accomplish this is to insert a multi-line comment into the function definition. Here's an updated version of the demo function I used last time.

function Get-FooDemo {
    <#
    .SYNOPSIS
    Get foo sample string
    .DESCRIPTION
    This is a description placeholder
    #>
    # ↓↓↓↓ This is my multiline metadata command ↓↓↓↓
    <#
        Source = D:\OneDrive\behind\2026\comment-tagging\code.ps1
        Author = Jeff Hicks
        Tags   = demo,scripting,metadata
        LastUpdate = 7/30/2026 1:44 PM
        Version = 0.9.0
    #>
    [cmdletbinding()]

    param(
        [string]$Name = 'PowerShell'
    )
    begin {
        Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN  ] Starting $($MyInvocation.MyCommand)"
    } #begin
    process {
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Processing"
        $r = $name.ToUpper()
        <#
            write ANSI formatted output
            using $PSStyle
        #>
        '{0}{1}{2}' -f $PSStyle.Foreground.BrightGreen, $r, $PSStyle.Reset
    } #process
    end {
        Write-Verbose "[$((Get-Date).TimeOfDay) END    ] Ending $($MyInvocation.MyCommand)"
    } #end
}

The date time value I inserted after copying the current date and time to the clipboard.

Get-Date -format g | Set-Clipboard

Again, it doesn't matter where you put the comment block. I like having it towards the beginning of my code. You could insert it like this:

# Source = D:\OneDrive\behind\2026\comment-tagging\code.ps1
# Author = Jeff Hicks
# Tags   = demo,scripting,metadata
# LastUpdate = 7/30/2026 1:44 PM
# Version = 0.9.0

But then you would have use the AST to parse out five individual lines. Using <#..#> means only dealing with a single result.

AST Parsing

With the function loaded into my PowerShell session, I can retrieve the script block and parse it with the AST, looking for my metadata comment block.

New-Variable astTokens -Force
New-Variable astErr -Force
$sb = (Get-Item Function:\Get-FooDemo).ScriptBlock
$ast = [System.Management.Automation.Language.Parser]::ParseInput($sb, [ref]$astTokens, [ref]$astErr)

$find = ($astTokens).where({ ($_.kind -eq 'comment') -and ($_.Text -match '\<#((\s)?)*Source') })

I'm using a regex pattern to match on the text. This code will work best if Source is the first item. Remember what I said about consistency?

PS C:\> $find.text
<#
        Source = D:\OneDrive\behind\2026\comment-tagging\code.ps1
        Author = Jeff Hicks
        Tags   = demo,scripting,metadata
        LastUpdate = 7/30/2026 1:44 PM
        Version = 0.9.0
    #>
PS C:\>

Next, I'll strip off the open and close comment characters and split the string into multiple lines.

$m = $find.Text.split("`n").Where({ $_ -NotMatch '#' }).Trim()

This leaves:

Source = D:\OneDrive\behind\2026\comment-tagging\code.ps1
Author = Jeff Hicks
Tags   = demo,scripting,metadata
LastUpdate = 7/30/2026 1:14 PM
Version = 0.9.0

As I did last time, I can split each line and add to a hashtable. Although I want to take an extra step. Everything I parse will be treated as a string. I definitely want the tags to be an array of strings and it might be useful to have LastUpdate be a [DateTime] object.

$metadata = [ordered]@{
    PSTypename = 'psFunctionMetadata'
}
foreach ($line in $m) {
    $split = $line.split('=')
    $name = $split[0].Trim()
    #treat the value as an appropriate time
    $value = switch ($name) {
        'LastUpdate' { $split[1].Trim() -as [DateTime] }
        'Tags' { $split[1].Trim().split(',') }
        default { $split[1].Trim() -as [string] }
    }
    $metadata.Add($split[0].Trim(), $value )
}

I am creating an [ordered] hashtable so that the line order will be preserved. If I end up using custom format files, then I don't have to worry about this.

I'm using a Switch statement to determine how to process each split line. I could have also treated Version accordingly, but I have something planned and that will complicate things so I'll leave it as string.

I can turn this into an object with properly typed properties.

PS C:\> [PSCustomObject]$metadata

Source     : D:\OneDrive\behind\2026\comment-tagging\code.ps1
Author     : Jeff Hicks
Tags       : {demo, scripting, metadata}
LastUpdate : 7/30/2026 1:44:00 PM
Version    : 0.9.0

I could start building tooling around this, but I'm always eager to push the envelope.

Using Structured Data

I know I can pull the data from the script block using the AST. Why not store the metadata as structured data? A CSV format doesn't work because tags might be an array. The quick answers are JSON or XML.

> If I really wanted a challenge I suppose I could use YAML, but that's crazy talk.

I think using JSON gives me the best option for handling hierarchical data is a minimal string format.

I test by re-using the existing metadata object.

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.