Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
July 22, 2025

I Stream of PowerShell - Part 2

Let's back into alternate data streams and experiment with ways to leverage them using PowerShell. What I like is the fact that you can add information to a file without changing the file itself, other than the last write time property. For a PowerShell script file, something that isn't part of a module, it might be useful to use the alternate data stream to store a file version value.

Set-Content -path C:\temp\Get-PSFoo.ps1 -Stream "!Version" -Value "1.0.0"

The alternate data stream is named !Version. I don't think there are any restrictions on the name. I suppose it is possible some other application might define a Version data stream. By using the ! character, I am hoping to avoid any conflicts with other applications. But I don't think it ultimately matters.

PS C:\> Get-Item -Path C:\temp\Get-PSFoo.ps1 -Stream '!Version'

PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\temp\Get-PSFoo.ps1:!Version
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\temp
PSChildName   : Get-PSFoo.ps1:!Version
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName      : C:\temp\Get-PSFoo.ps1
Stream        : !Version
Length        : 7

PS C:\> Get-Content -Path C:\temp\Get-PSFoo.ps1 -Stream '!Version'
1.0.0

However, be careful with the stream values. You might try storing a [System.Version] object in the stream.

[Version]$ver = '1.1.123'
Set-Content -path C:\temp\Get-PSFoo.ps1 -Stream "!Version" -Value $ver

However the content will be stored as a string.

PS C:\> Get-Content -path C:\temp\Get-PSFoo.ps1 -Stream "!Version" | Tee -Variable v
1.1.123
PS C:\> $v -is [version]
False
PS C:\> $v -is [string]

If the value is not already a string, it appears that PowerShell will convert it to a string using the ubiquitous ToString() method.

PS C:\> set-Content -Path C:\temp\Get-PSFoo.ps1 -Stream "psVer" -Value $PSVersionTable
PS C:\> Get-Content -path C:\temp\Get-PSFoo.ps1 -Stream "psVer"
System.Management.Automation.PSVersionHashTable
PS C:\> $PSVersionTable.ToString()
System.Management.Automation.PSVersionHashTable

This shouldn't be that much of problem, since you are likely to be storing simple string values like a script author.

Set-Content -path C:\temp\Get-PSFoo.ps1 -Stream Author -Value "Jeff Hicks"
Set-Content -path C:\temp\Get-PSFoo.ps1 -Stream Computername -Value $env:Computername

One allowed variant is that you can store an array of strings in the stream.

PowerShell Tooling

Before we get too far, let's add some PowerShell tooling to identify alternate data streams and their values. I could write one function to do everything. But if you think about it, there are really two different tasks. One is to identify the streams for a file and the other is to read the stream values. So I will create two functions. But write them so that they can be used together in the PowerShell pipeline.

The first function will identify alternate data streams other than the default $DATA stream.

function Get-FileStreamInfo {
    [cmdletbinding()]
    [OutputType('PSFileStreamInfo')]
    param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path
    )
    process {
        $streams = Get-Item -Path $Path -Stream * | Where-Object { $_.Stream -ne ':$DATA' }
        [PSCustomObject]@{
            PSTypeName = 'PSFileStreamInfo'
            Path       = Convert-Path $Path
            Streams    = $streams.Stream
        }
    }
}

The second function will read the stream values. I will use a custom object type for each function so that I can easily identify the output.

function Get-FileStreamData {
    [cmdletbinding()]
    [OutputType('PSFileStreamData','HashTable')]
    param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            HelpMessage = 'The path to the file'
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,

        [Parameter(
            Position = 1,
            Mandatory,
            ValueFromPipelineByPropertyName,
            HelpMessage = 'The stream names to retrieve data from.'
        )]
        [string[]]$Streams,

        [Parameter(HelpMessage = 'Return the data as a hash table instead of a custom object for each stream.')]
        [switch]$AsHashTable
    )
    process {
        $Path = Convert-Path $Path
        if ($Streams.Count -gt 0) {
            Write-Verbose "Processing $($Streams.Count) stream(s) in $($Path)"
            if ($AsHashTable) {
                $data = [ordered]@{Path = $Path}
                foreach ($stream in $Streams) {
                    Write-Verbose "Processing stream: $stream"
                    $data[$stream] = Get-Content -Path $Path -Stream $stream
                }`
                $data
            }
            else {
                foreach ($stream in $Streams) {
                    Write-Verbose $stream
                    [PSCustomObject]@{
                        PSTypeName = 'PSFileStreamData'
                        Path       = $Path
                        Stream     = $stream
                        Data       = Get-Content -Path $Path -Stream $stream
                    }
                } #foreach
            }
        }
        else {
            Write-Verbose "No streams found in $($Path)"
        }
    }
}

Let's try them out.

PS C:\> Get-FileStreamInfo C:\temp\Get-PSFoo.ps1

Path                  Streams
----                  -------
C:\temp\Get-PSFoo.ps1 {!Version, Author, Computername}

I wrote the function to take pipeline input so I can easily identify the streams for multiple files.

PS C:\> dir c:\temp -file | Get-FileStreamInfo | Where Streams

Path                    Streams
----                    -------
C:\temp\2112.mp3        01APIC_00.jpg
C:\temp\Get-PSFoo.ps1   {!Version, Author, Computername}
C:\temp\png.zip         Demo
C:\temp\RollChanges.mp3 01APIC_00.jpg
C:\temp\walrus.txt      {jeff, ps}

Once I know the stream name, I can get the stream data.

PS C:\> Get-FileStreamData C:\temp\Get-PSFoo.ps1 -Streams author,computername

Path                  Stream       Data
----                  ------       ----
C:\temp\Get-PSFoo.ps1 author       Jeff Hicks
C:\temp\Get-PSFoo.ps1 computername CADENZA

By default this function writes custom objects for each stream. This design best follows the practice of having a function write one type of object to the pipeline. However, I thought there might be situations where I might want to have a single object with all the stream data. For this scenario, I can use a hashtable. This is ideal when joining the two functions together in the pipeline.

PS C:\> Get-FileStreamInfo C:\temp\Get-PSFoo.ps1 | Get-FileStreamData -AsHashTable

Name                           Value
----                           -----
Path                           C:\temp\Get-PSFoo.ps1
!Version                       1.1.123
Author                         Jeff Hicks
Computername                   CADENZA

I have a feeling we will be revisiting this output later.

Metadata at Scale

To test this at scale, I copy 25 random PowerShell script files to my TEMP folder. I can quickly write author information.

dir c:\temp\*.ps1 |Foreach-Object {
    #save last write time values so they can be restored later
    $lwt = $_.LastWriteTime
    $lwtUTC = $_.LastWriteTimeUtc
    Set-Content -Path $_.FullName -Stream Author -Value "Jeff Hicks"
    $_.LastWriteTime = $lwt
    $_.LastWriteTimeUtc = $lwtUTC
}

This code snippet saves the last write time values so they can be restored after updating the stream. This might have an impact on version control or backup, so take this into consideration. this code snippet can easily be turned into a PowerShell function that accepts pipeline input.

I can verify the change with my Get-FileStreamData function.

PS C:\> dir c:\temp\*.ps1 | Select -first 10 | Get-FileStreamData -Streams author

Path                               Stream Data
----                               ------ ----
C:\temp\Blog-ScanningNotes.ps1     author Jeff Hicks
C:\temp\Create-AllProfiles.ps1     author Jeff Hicks
C:\temp\Demo-Information.ps1       author Jeff Hicks
C:\temp\Demo-PowerShellHTML.ps1    author Jeff Hicks
C:\temp\demopullconfig-change.ps1  author Jeff Hicks
C:\temp\dev-windowtime.ps1         author Jeff Hicks
C:\temp\Download-2016Eval.ps1      author Jeff Hicks
C:\temp\DSC-BeNothing.ps1          author Jeff Hicks
C:\temp\Get-LocalAdministrator.ps1 author Jeff Hicks
C:\temp\Get-PSFoo.ps1              author Jeff Hicks

Summary

This is a good place to take a break. I want to give you time to look at my code samples and try them out. When we pick up this topic again, we'll combine this concept with the PowerShell AST.

(c) 2022-2025 JDH Information Technology Solutions, Inc. - all rights reserved
Don't miss what's next. Subscribe to Behind the PowerShell Pipeline:
Start the conversation:
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.