Dot-sourcing a script without executing it - powershell

Is it possible in Powershell to dot-source or re-use somehow script functions without it being executed? I'm trying to reuse the functions of a script, without executing the script itself. I could factor out the functions into a functions only file but I'm trying to avoid doing that.
Example dot-sourced file:
function doA
{
Write-Host "DoAMethod"
}
Write-Host "reuseme.ps1 main."
Example consuming file:
. ".\reuseme.ps1"
Write-Host "consume.ps1 main."
doA
Execution results:
reuseme.ps1 main.
consume.ps1 main.
DoAMethod
Desired result:
consume.ps1 main.
DoAMethod

You have to execute the function definitions to make them available. There is no way around it.
You could try throwing the PowerShell parser at the file and only executing function definitions and nothing else, but I guess the far easier approach would be to structure your reusable portions as modules or simply as scripts that don't do anything besides declaring functions.
For the record, a rough test script that would do exactly that:
$file = 'foo.ps1'
$tokens = #()
$errors = #()
$result = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors)
$tokens | %{$s=''; $braces = 0}{
if ($_.TokenFlags -eq 'Keyword' -and $_.Kind -eq 'Function') {
$inFunction = $true
}
if ($inFunction) { $s += $_.Text + ' ' }
if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'LCurly') {
$braces++
}
if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'RCurly') {
$braces--
if ($braces -eq 0) {
$inFunction = $false;
}
}
if (!$inFunction -and $s -ne '') {
$s
$s = ''
}
} | iex
You will have problems if functions declared in the script reference script parameters (as the parameter block of the script isn't included). And there are probably a whole host of other problems that can occur that I cannot think of right now. My best advice is still to distinguish between reusable library scripts and scripts intended to be invoked.

After your function, the line Write-Host "reuseme.ps1 main." is known as "procedure code" (i.e., it is not within the function). You can tell the script not to run this procedure code by wrapping it in an IF statement that evaluates $MyInvocation.InvocationName -ne "."
$MyInvocation.InvocationName looks at how the script was invoked and if you used the dot (.) to dot-source the script, it will ignore the procedure code. If you run/invoke the script without the dot (.) then it will execute the procedure code. Example below:
function doA
{
Write-Host "DoAMethod"
}
If ($MyInvocation.InvocationName -ne ".")
{
Write-Host "reuseme.ps1 main."
}
Thus, when you run the script normally, you will see the output. When you dot-source the script, you will NOT see the output; however, the function (but not the procedure code) will be added to the current scope.

The best way to re-use code is to put your functions in a PowerShell module. Simply create a file with all your functions and give it a .psm1 extension. You then import the module to make all your functions available. For example, reuseme.psm1:
function doA
{
Write-Host "DoAMethod"
}
Write-Host "reuseme.ps1 main."
Then, in whatever script you want to use your module of functions,
# If you're using PowerShell 2, you have to set $PSScriptRoot yourself:
# $PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Import-Module -Name (Join-Path $PSScriptRoot reuseme.psm1 -Resolve)
doA

While looking a bit further for solutions for this issue, I came across a solution which is pretty much a followup to the hints in Aaron's answer. The intention is a bit different, but it can be used to achieve the same result.
This is what I found:
https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
It needed it for some testing with Pester where I wanted to avoid changing the structure of the file before having written any tests for the logic.
It works quite well, and gives me the confidence to write some tests for the logic first, before refactoring the structure of the files so I no longer have to dot-source the functions.
Describe "SomeFunction" {
# Import the ‘SomeFunction’ function into the current scope
. (Get-FunctionDefinition –Path $scriptPath –Function SomeFunction)
It "executes the function without executing the script" {
SomeFunction | Should Be "fooBar"
}
}
And the code for Get-FunctionDefinition
#Requires -Version 3
<#
.SYNOPSIS
Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
Returns a function's source definition as a Powershell ScriptBlock from an
external .ps1 file or existing ScriptBlock. This module is primarily
intended to be used to test private/nested/internal functions with Pester
by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
The source function's name to return as a [ScriptBlock].
.PARAMETER Path
Path to a Powershell script file that contains the source function's
definition.
.PARAMETER LiteralPath
Literal path to a Powershell script file that contains the source
function's definition.
.PARAMETER ScriptBlock
A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
If the following functions are defined in a file named 'PrivateFunction.ps1'
function PublicFunction {
param ()
function PrivateFunction {
param ()
Write-Output 'InnerPrivate'
}
Write-Output (PrivateFunction)
}
The 'PrivateFunction' function can be tested with Pester by dot-sourcing
the required function in the either the 'Describe', 'Context' or 'It'
scopes.
Describe "PrivateFunction" {
It "tests private function" {
## Import the 'PrivateFunction' definition into the current scope.
. (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
PrivateFunction | Should BeExactly 'InnerPrivate'
}
}
.LINK
https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
#>
function Get-FunctionDefinition {
[CmdletBinding(DefaultParameterSetName='Path')]
[OutputType([System.Management.Automation.ScriptBlock])]
param (
[Parameter(Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
ParameterSetName='Path')]
[ValidateNotNullOrEmpty()]
[Alias('PSPath','FullName')]
[System.String] $Path = (Get-Location -PSProvider FileSystem),
[Parameter(Position = 0,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = 'LiteralPath')]
[ValidateNotNullOrEmpty()]
[System.String] $LiteralPath,
[Parameter(Position = 0,
ValueFromPipeline = $true,
ParameterSetName = 'ScriptBlock')]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.ScriptBlock] $ScriptBlock,
[Parameter(Mandatory = $true,
Position =1,
ValueFromPipelineByPropertyName = $true)]
[Alias('Name')]
[System.String] $Function
)
begin {
if ($PSCmdlet.ParameterSetName -eq 'Path') {
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path);
}
elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
## Set $Path reference to the literal path(s)
$Path = $LiteralPath;
}
} # end begin
process {
$errors = #();
$tokens = #();
if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
$ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors);
}
else {
$ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors);
}
[System.Boolean] $isFunctionFound = $false;
$functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true);
foreach ($f in $functions) {
if ($f.Name -eq $Function) {
Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text));
$isFunctionFound = $true;
}
} # end foreach function
if (-not $isFunctionFound) {
if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
$errorMessage = 'Function "{0}" not defined in script block.' -f $Function;
}
else {
$errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path;
}
Write-Error -Message $errorMessage;
}
} # end process
} #end function Get-Function

Related

Pass [switch] inside Start-Job

There is a script for users to log in, it calls other scripts in turn, depending on the conditions.
In order to call scripts separately manually, the [switch]$Silent parameter has been added. Question - how to pass this parameter inside Start-Job? I tried to add to the list of arguments in different ways - the value always falls into the neighboring parameter, regardless of the order.
Main script example
Param(
[string]$location = 'C:\Users',
[switch]$Silent
)
Start-Job -FilePath ".\Fonts_Install.ps1" -ArgumentList ($Silent,$location) | Wait-Job
Fonts_Install.ps1
Param(
[switch]$Silent = $false,
[string]$location = '.'
)
$path_fonts = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts"
$Registry = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
function WriteLog {
Param ([string]$LogString)
$Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
$LogMessage = "$Stamp $LogString"
Add-content $LogFile -value $LogMessage
}
$Logfile = "$env:LOCALAPPDATA\Temp\fonts_install.log"
WriteLog "Silent $Silent"
WriteLog "location $location"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName PresentationCore
$SourceFolder = "$location\Fonts_Install"
$WindowsFonts = [System.Drawing.Text.PrivateFontCollection]::new()
$Fonts = Get-ChildItem -Path $SourceFolder -Include *.ttf, *.otf -Recurse -File
ForEach ($Font in $Fonts) {
$Font_Name = $Font.Name
$font_fullname = $Font.fullname
if (Test-Path -PathType Leaf -Path "$path_fonts\$Font_Name") {
WriteLog "Previously installed $Font_Name"
}
else {
Copy-Item $Font -Destination "$path_fonts" -Force -Confirm:$false -PassThru
$WindowsFonts.AddFontFile("$font_fullname")
$ValueFont = "$path_fonts" + "\" + "$Font_Name"
$Typeface = New-Object -TypeName Windows.Media.GlyphTypeface -ArgumentList "$font_fullname"
[string]$FamilyFaceNames = $Typeface.FamilyNames.Values + $Typeface.FaceNames.Values
$RegistryValue = #{
Path = $Registry
Name = $FamilyFaceNames
Value = $ValueFont
}
if (Test-Path $Registry\$FamilyFaceNames) {
Remove-ItemProperty -name $FamilyFaceNames -path $Registry
}
New-ItemProperty #RegistryValue
WriteLog "New fonts installed $Font_Name"
}
}
switch ($Silent) {
$false {
if ($Error.Count -gt 0) {
for ($i = 0; $i -le ($Error.Items.Count + 1); $i++) {
$errMSG = "$Error"
}
[System.Windows.Forms.MessageBox]::Show("$errMSG", "Error", "OK", "Error")
}
else {
[System.Windows.Forms.MessageBox]::Show("ок", "Fonts", "OK", "Asterisk") | out-null
}
}
}
Unfortunately, specifying pass-through arguments via Start-Job's -ArgumentList (-Args) is limited to positional arguments, which prevents binding [switch] parameters, whose arguments must by definition be named.
As a workaround, instead of using -FilePath, invoke your script via the -ScriptBlock parameter. Inside of a script block ({ ... }, named arguments may be used in script calls, as usual:
Start-Job -ScriptBlock {
# Set the current location to the same location as the caller.
# Note: Only needed in *Windows PowerShell*.
Set-Location -LiteralPath ($using:PWD).ProviderPath
.\Fonts_Install.ps1 -Silent:$using:Silent $using:Location
} | Receive-Job -Wait -AutoRemoveJob
Note the use of the $using: scope in order to embed variable values from the caller's scope in the script block that will execute in the background.
You still need to refer to the -Silent parameter by name, and the whether the switch is on or off can be communicated by appending :$true or :$false to it, which is what :$using:Silent does.
In Windows PowerShell, background jobs execute in a fixed location (working directory), namely the user's Documents folder, hence the Set-Location call to explicitly use the same location as the caller, so that the script file can be referenced by a relative path (.\). This is no longer necessary in PowerShell (Core) 7+, which now thankfully uses the same location as the calller.
Here is a different alternative to mklement0's helpful answer, this answer does not use Start-Job and uses a PowerShell instance instead, using this method we can leverage the automatic variable $PSBoundParameters.
Do note, that for this to work properly, both .ps1 scripts must share the same parameter names or Alias Attribute Declarations that matches the same parameter from the caller. See this answer for more details.
You can use these snippets below as a example for you to test how it works.
caller.ps1
param(
[string] $Path = 'C:\Users',
[switch] $Silent
)
try {
if(-not $PSBoundParameters.ContainsKey('Path')) {
$PSBoundParameters['Path'] = $Path
}
$ps = [powershell]::Create().
AddCommand('path\to\myScript.ps1').
AddParameters($PSBoundParameters)
$iasync = $ps.BeginInvoke()
# Do something else here while the .ps1 runs
# ...
# Get async result from the PS Instance
$ps.EndInvoke($iasync)
}
finally {
if($ps -is [IDisposable]) {
$ps.Dispose()
}
}
myScript.ps1
# Note, since we're bounding this parameters from the caller.ps1,
# We don't want to assign Default Values here!
param(
[string] $Path,
[switch] $Silent
)
foreach($param in $MyInvocation.MyCommand.Parameters.Keys) {
[pscustomobject]#{
Parameter = $param
Value = Get-Variable $param -ValueOnly
}
}
A few examples:
PS /> .\caller.ps1
Parameter Value
--------- -----
Path C:\Users
Silent False
PS /> .\caller.ps1 -Path hello
Parameter Value
--------- -----
Path hello
Silent False
PS /> .\caller.ps1 -Path world -Silent
Parameter Value
--------- -----
Path world
Silent True

How do you write a Pester mock for task sequence variable setting

I am working on writing Pester tests for our PowerShell scripts that are used during task sequences. Several of them work with the task sequence variables and so I wrote a mock that allows for testing reading variables and am now trying to figure out how to do it for writing variables.
This is the code to read a task sequence variable:
$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$Value = $TsEnv.Value('VariableNameToRead')
By passing in the $TsEnv to a function I can then mock it with the following:
$TsEnv = #{
'VariableNameToRead' = 'TestValue'
}
Add-Member -InputObject $TsEnv -MemberType ScriptMethod -Name Value -Value {
Param( [String]$Key )
$This[$Key]
}
This is the code for writing a task sequence variable:
$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$TsEnv.Value('VariableNameToWrite') = 'ValueToWrite'
With it being in parentheses after the $TsEnv.Value I am thinking it is treating it as a method, but I am unable to find any examples on how to assign values to a method.
With Pester 4.3.3+, you might be able to use New-MockObject to create a usable mock of that COM object.
Alternatively, you can do something similar to the below to allow you to mock the functionality of the COM object.
If that COM object is available on the machines where your CI is running, I might consider skipping the mocks and writing an integration test.
# functions.ps1
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";
function GetTaskSequenceValue(
[Parameter(Mandatory=$true)]
[string] $varNameToRead,
[Parameter(Mandatory=$false)]
[System.Management.Automation.ScriptBlock] $readAction = {
param([string] $readKey)
$tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
$tsEnv.Value($readKey)
}
) {
$value = Invoke-Command `
-ScriptBlock $readAction `
-ArgumentList #($varNameToRead)
return $value
}
function SetTaskSequenceValue(
[Parameter(Mandatory=$true)]
[string] $varNameToWrite,
[Parameter(Mandatory=$false)]
[System.Management.Automation.ScriptBlock] $writeAction = {
param([string] $writeKey, [string] $value)
$tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
$TsEnv.Value($writeKey) = $value
}
) {
try {
Invoke-Command `
-ScriptBlock $writeAction `
-ArgumentList #($varNameToWrite)
return $true
}
catch {
# Swallow it
}
return $false
}
Tests for the functions abov. The tests manually mock out the COM objects
# functions.test.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";
Describe "GetTaskSequenceValue" {
It "gets the expected value" {
$expected = 'value'
$mockAction = {
param($dummy)
return 'value'
}
$actual = GetTaskSequenceValue `
-varNameToRead 'dummyName' `
-readAction $mockAction
$actual | Should Be $expected
}
}
Describe "SetTaskSequenceValue" {
It "sets the expected value" {
$expected = 'value'
$mockAction = {
param($dummy)
return 'value'
}
$actual = SetTaskSequenceValue `
-varNameToWrite 'dummyValue' `
-writeAction $mockAction
$actual | Should Be $true
}
}
Anything to deal with getting environment variables, WMI, or dotnet static method calls, I like to contain within a small helper function, then it's very easy to mock it. Here's what that helper could look like.
Function Get-SMSTsVariable{($VariableName)
return $TSEnv.Value($VariableName)
}
Then you can easily mock this in various contexts to check and see how your code acts when various environmental variables are set.
For instance, if you want it to return a value of BitLockerProvisioning when you run Get-SMSTsVariable -VariableName _SMSTSCurrentActionName, and to return 'C:' when you run _OSDDetectedWinDir you setup a mock like this:
mock Get-SMSTsVariable `
-parameterFilter { $VariableName -eq '_SMSTSCurrentActionName'} `
-mockWith {return 'BitLockerProvisioning'}
mock Get-SMSTsVariable `
-parameterFilter { $VariableName -eq '_OSDDetectedWinDir'} `
-mockWith {return 'C:'}
In this way, you can begin your test setting up a handful of responses for the various ways your functions operate. It's really a breeze.

Passing CLI arguments to a function in PowerShell

I have a PowerShell script that contains several functions. One of the functions should receive command line arguments. Here's a similar script to what I'm doing:
$mydocs = [Environment]::GetFolderPath("MyDocuments")
function example() {
Param(
[string]$name = "testfolder",
[switch]$foo = $false
)
$newdir = $mydocs + "\" + $name
if (!(Test-Path $newdir)) { mkdir $newdir }
}
example
So when I run this:
.\example.ps1 -name anewfolder -foo
I want the function example to use these arguments.
Because of scoping, my function has no $args. I put Write-Host $args into the function to double check, and it returns nothing. How can I pull the parent args, pass them into params, and get example to run with my arguments? Or should I just remove the function and run this part of my code in the parent scope?
If you want to pass arguments to your script, you need to capture them in a Param() block like your Function has. Alternatively, you can access script args with a scope selector like $script:Args.
Param([String]$Name,[Switch]$Foo)
$mydocs = [Environment]::GetFolderPath("MyDocuments")
function example
{
param(
[string]$name,
[switch]$foo
)
$newdir = Join-Path $mydocs $name
if (!(Test-Path $newdir)) { mkdir $newdir }
}
example -Name $Name -Foo
Now, .\example.ps1 -name anewfolder -foo will be successful.
$args inside the function is a different variable than $args in the global scope of the script. A very simple way of passing arbitrary script arguments to your function is to splat $args:
example #args
Note, however, that this isn't a very clean approach. It'd be better to properly parameterize the script and selectively pass those parameters that are meant to go to the function, as TheIncorrigible1 suggested.
Param(
[string]$name,
[int]$bar,
[switch]$foo
)
function example() {
Param(
[string]$name = "testfolder",
[switch]$foo
)
$newdir = "${mydocs}\${name}"
if (!(Test-Path $newdir)) { mkdir $newdir }
}
$params = #{}
if ($PSBoundParameters.ContainsKey('name')) { $params['name'] = $name }
if ($PSBoundParameters.ContainsKey('foo')) { $params['foo'] = $true }
example #params

Powershell Logging within modules and dot sourced scripts

I have a few functions stored in a .psm1 file that is used by several different ps1 scripts. I created a logging function (shown below), which I use throughout these ps1 scripts. Typically, I import the module within a script by simply calling something like:
Import-Module $PSScriptRoot\Module_Name.psm1
Then within the module, I have a write logger function:
Write-Logger -Message "Insert log message here." #logParams
This function is used throughout the main script and the module itself. The splatting parameter #logParams is defined in my main .ps1 file and is not explicitly passed to the module, I suppose the variables are implicitly within the module's scope upon being imported. What I have works, but I feel like it isn't a great practice. Would it be better practice to add a param block within my module to require #logParams to be explicitly passed from the main .ps1 script? Thanks!
function Write-Logger() {
[cmdletbinding()]
Param (
[Parameter(Mandatory=$true)]
[string]$Path,
[Parameter(Mandatory=$true)]
[string]$Message,
[Parameter(Mandatory=$false)]
[string]$FileName = "Scheduled_IDX_Backup_Transcript",
[switch]$Warning,
[switch]$Error
)
# Creates new log directory if it does not exist
if (-Not (Test-Path ($path))) {
New-Item ($path) -type directory | Out-Null
}
if ($error) {
$label = "Error"
}
elseif ($warning) {
$label = "Warning"
}
else {
$label = "Normal"
}
# Mutex allows for writing to single log file from multiple runspaces
$mutex = new-object System.Threading.Mutex $false,'MutexTest'
[void]$mutex.WaitOne()
Write-Host "$(Format-LogTimeStamp) $label`: $message"
"$(Format-LogTimeStamp) $label`: $message" | Out-file "$path\$fileName.log" -encoding UTF8 -append
[void]$mutex.ReleaseMutex()
}
I have this code in ps1 that I dot source into scripts where I want to produce my own logs. The ps1 contains the simpleLogger class as well as the routine below that creates a global variable. The script can be dot sourced again and the global variable value passed to subsequently spawned jobs to maintain a single log file.
class simpleLogger
{
[string]$source
[string]$target
[string]$action
[datetime]$datetime
hidden [string]$logFile = $global:simpleLog
simpleLogger()
{
$this.datetime = Get-Date
}
simpleLogger( [string]$source, [string]$target, [string]$action )
{
$this.action = $action
$this.source = $source
$this.target = $target
$this.datetime = Get-Date
}
static [simpleLogger] log( [string]$source, [string]$target, [string]$action )
{
$newLogger = [simpleLogger]::new( [string]$source, [string]$target, [string]$action )
do {
$done = $true
try {
$newLogger | export-csv -Path $global:simpleLog -Append -NoTypeInformation
}
catch {
$done = $false
start-sleep -milliseconds $(get-random -min 1000 -max 10000)
}
} until ( $done )
return $newLogger
}
}
if( -not $LogSession ){
$global:logUser = $env:USERNAME
$global:logDir = $env:TEMP + "\"
$startLog = (get-date).tostring("MMddyyyyHHmmss")
$global:LogSessionGuid = (New-Guid)
$global:simpleLog = $script:logDir+$script:logUser+"-"+$LogSessionGuid+".log"
[simpleLogger]::new() | export-csv -Path $script:simpleLog -NoTypeInformation
$global:LogSession = [simpleLogger]::log( $script:logUser, $LogSessionGuid, 'Log init' )
}

How do I find out a PowerShell module's ArgumentList?

I'm trying to write a script that needs to be detect what the ArgumentList of a PowerShell module is. Is there any way of finding this out?
The end game is to be able to use this to create a simple DI container for loading modules.
You can use the AST parser to show you the param() block of the module file. Maybe use Get-Module to find out information about where the module files are located, then parse those and walk the AST to get the information you're after. Does this seem like something that would be useful?
function Get-ModuleParameterList {
[CmdletBinding()]
param(
[string] $ModuleName
)
$GetModParams = #{
Name = $ModuleName
}
# Files need -ListAvailable
if (Test-Path $ModuleName -ErrorAction SilentlyContinue) {
$GetModParams.ListAvailable = $true
}
$ModuleInfo = Get-Module #GetModParams | select -First 1 # You'll have to work out what to do if more than one module is found
if ($null -eq $ModuleInfo) {
Write-Error "Unable to find information for '${ModuleName}' module"
return
}
$ParseErrors = $null
$Ast = if ($ModuleInfo.RootModule) {
$RootModule = '{0}\{1}' -f $ModuleInfo.ModuleBase, (Split-Path $ModuleInfo.RootModule -Leaf)
if (-not (Test-Path $RootModule)) {
Write-Error "Unable to determine RootModule for '${ModuleName}' module"
return
}
[System.Management.Automation.Language.Parser]::ParseFile($RootModule, [ref] $null, [ref] $ParseErrors)
}
elseif ($ModuleInfo.Definition) {
[System.Management.Automation.Language.Parser]::ParseInput($ModuleInfo.Definition, [ref] $null, [ref] $ParseErrors)
}
else {
Write-Error "Unable to figure out module source for '${ModuleName}' module"
return
}
if ($ParseErrors.Count -ne 0) {
Write-Error "Parsing errors detected when reading RootModule: ${RootModule}"
return
}
$ParamBlockAst = $Ast.Find({ $args[0] -is [System.Management.Automation.Language.ParamBlockAst] }, $false)
$ParamDictionary = [ordered] #{}
if ($ParamBlockAst) {
foreach ($CurrentParam in $ParamBlockAst.Parameters) {
$CurrentParamName = $CurrentParam.Name.VariablePath.UserPath
$ParamDictionary[$CurrentParamName] = New-Object System.Management.Automation.ParameterMetadata (
$CurrentParamName,
$CurrentParam.StaticType
)
# At this point, you can add attributes to the ParameterMetaData instance based on the Attribute
}
}
$ParamDictionary
}
You should be able to give that a module name or the path to a module. It's barely been tested, so there are probably some instances where it won't work. Right now, it returns a dictionary like viewing the 'Parameters' property returned from Get-Command. If you want the attribute information, you'd need to do a little work to build each one.