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
}