Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
August 5, 2025

I Stream of PowerShell - Part 3

Let's pick up where we left off in exploring how to take advantage of NTFS alternate data streams in PowerShell. If you need a refresher, take a look at Part 1 and Part 2.

At the end of last year, I left you with a scripting challenge. I shared the code for my approach in January. My approach was to use the Abstract Syntax Tree (AST) to parse the script file and create metadata which I then inserted as a comment at the end of the script file. Instead of inserting the metadata directly into the script file, I could use an alternate data stream to store the metadata. Let's see how we might accomplish that.

New Script Metadata Class

I took my original solution and revised and refactored it to use a PowerShell class to represent the metadata. The class is called PSScriptMetadata.

class PSScriptMetadata {
    [string]$Path
    [string]$Name
    [datetime]$LastModified
    [version]$RequiredVersion
    [bool]$RequiresAdmin
    [string[]]$RequiredModules
    [string[]]$Commands
    #these could be saved as separate and individual properties
    [string]$Author
    [string]$Comment
    #Using .NET to make this cross platform
    [string]$ComputerName = [System.Environment]::MachineName

    #Object export methods
    [string[]]ToJson() {
        $json = $this | ConvertTo-Json -Depth 5
        return $json
    }
    [string]ToXml() {
        $xml = $this | ConvertTo-Xml -As String
        return $xml
    }
    [string[]]ToClixml () {
        $clixml = $this | ConvertTo-CliXml -Depth 5
        return $clixml
    }

    PSScriptMetadata([string]$Path) {
        $this.Path = $Path
        $this.Name = Split-Path -Path $Path -Leaf
        $this.LastModified = (Get-Item $Path).LastWriteTime
    }
}

> An argument could be made to save the Author, Comment, and ComputerName properties as separate and alternate data streams. If you use this code, feel free to adapt it to your needs.

This also meant a revised function to employ the class. This PowerShell 7 version of the function:

  • removes Tags property - I will tackle that later.
  • adds an option to add an author
  • adds an option for a general comment
  • adds the computer name
  • store last file update
function New-ScriptMetadata {
    [cmdletbinding()]
    [OutputType('PSScriptMetadata')]
    [alias('nsm')]
    param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            HelpMessage = 'The path to the .PS1 script file'
        )]
        [ValidatePattern('.*\.ps1$')]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,
        [Parameter(HelpMessage = 'Enter optional author information.')]
        [string]$Author,
        [Parameter(HelpMessage = 'Enter a brief general comment about the script file.')]
        [string]$Comment
    )

    begin {
        #the command exclusion file is a list of commands that should not be included in the metadata
        $CmdExclude = Get-Content -Path C:\scripts\CmdExclude.txt |
        Where-Object { $_ -NotMatch '^#' -and $_ -match '\w+' }
        #initialize required AST variables
        New-Variable astTokens -Force
        New-Variable astErr -Force
    } #begin
    process {
        #convert to a file system path
        $Path = Convert-Path $Path
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Processing $Path"
        $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)
        $RequiredVersion = $AST.ScriptRequirements.RequiredPSVersion
        $RequiresAdmin = $AST.ScriptRequirements.IsElevationRequired
        $RequiredModules = $AST.ScriptRequirements.RequiredModules.Name
        $cmdGroup = $astTokens.where({ $_.TokenFlags -eq 'CommandName' -and $_.Value -notin $CmdExclude }) |
        Group-Object Text
        $commands = $cmdGroup.Name

        $meta = [PSScriptMetadata]::new($Path)

        #Update the object
        $meta.RequiredVersion = $RequiredVersion ? $RequiredVersion : $Null
        $meta.RequiresAdmin = $RequiresAdmin
        $meta.RequiredModules = $RequiredModules ? $RequiredModules : @()
        $meta.Commands = $commands ? $commands : @()
        $meta.Author = $Author ? $Author : $Null
        $meta.Comment = $Comment ? $Comment : $Null
        $meta

    } #process
    end {
        #not used
    } #end
}
Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.